diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..69c153ce --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +disable_warnings = already-imported + +[report] +include = aurweb/* +fail_under = 85 +exclude_lines = + if __name__ == .__main__.: + pragma: no cover diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..6ec5547d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +*/*.mo +conf/config +conf/config.sqlite +conf/config.sqlite.defaults +conf/docker +conf/docker.defaults diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..5a751aad --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# EditorConfig configuration for aurweb +# https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{php,t}] +indent_style = tab diff --git a/.env b/.env new file mode 100644 index 00000000..22846cb4 --- /dev/null +++ b/.env @@ -0,0 +1,9 @@ +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/" +TEST_RECURSION_LIMIT=10000 +COMMIT_HASH= diff --git a/.gitignore b/.gitignore index 7c9fa60b..8388694c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,45 @@ +__pycache__/ +*.py[cod] +.vim/ +.pylintrc +.coverage +.idea +/cache/* +/logs/* +/build/ +/dist/ +/aurweb.egg-info/ +/personal/ +/notes/ +/vendor/ +/pyrightconfig.json +/taskell.md +aur.git/ +aurweb.sqlite3 +conf/config +conf/config.sqlite +conf/config.sqlite.defaults +conf/docker +conf/docker.defaults +data.sql dummy-data.sql* +env/ +fastapi_aw/ +htmlcov/ po/*.mo po/*.po~ po/POTFILES -web/locale/*/ -aur.git/ -__pycache__/ -*.py[cod] -test/test-results/ schema/aur-schema-sqlite.sql +test/test-results/ +test/trash directory* +web/locale/*/ +web/html/*.gz + +# Do not stage compiled asciidoc: make -C doc +doc/rpc.html + +# Ignore any user-configured .envrc files at the root. +/.envrc + +# Ignore .python-version file from Pyenv +.python-version diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..d682a9f5 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,81 @@ +image: archlinux:base-devel +cache: + key: system-v1 + paths: + # For some reason Gitlab CI only supports storing cache/artifacts in a path relative to the build directory + - .pkg-cache + +variables: + AUR_CONFIG: conf/config # Default MySQL config setup in before_script. + DB_HOST: localhost + TEST_RECURSION_LIMIT: 10000 + CURRENT_DIR: "$(pwd)" + LOG_CONFIG: logging.test.conf + +test: + stage: test + tags: + - fast-single-thread + before_script: + - export PATH="$HOME/.poetry/bin:${PATH}" + - ./docker/scripts/install-deps.sh + - ./docker/scripts/install-python-deps.sh + - useradd -U -d /aurweb -c 'AUR User' aur + - ./docker/mariadb-entrypoint.sh + - (cd '/usr' && /usr/bin/mysqld_safe --datadir='/var/lib/mysql') & + - 'until : > /dev/tcp/127.0.0.1/3306; do sleep 1s; done' + - cp -v conf/config.dev conf/config + - sed -i "s;YOUR_AUR_ROOT;$(pwd);g" conf/config + - ./docker/test-mysql-entrypoint.sh # Create mysql AUR_CONFIG. + - make -C po all install # Compile translations. + - make -C doc # Compile asciidoc. + - make -C test clean # Cleanup coverage. + script: + # Run sharness. + - make -C test sh + # Run pytest. + - pytest + - make -C test coverage # Produce coverage reports. + - flake8 --count aurweb # Assert no flake8 violations in aurweb. + - flake8 --count test # Assert no flake8 violations in test. + - flake8 --count migrations # Assert no flake8 violations in migrations. + - isort --check-only aurweb # Assert no isort violations in aurweb. + - isort --check-only test # Assert no flake8 violations in test. + - isort --check-only migrations # Assert no flake8 violations in migrations. + coverage: '/TOTAL.*\s+(\d+\%)/' + artifacts: + reports: + cobertura: coverage.xml + +deploy: + 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 + + environment: + name: development + url: https://aur-dev.archlinux.org diff --git a/.gitlab/issue_templates/Account Request.md b/.gitlab/issue_templates/Account Request.md new file mode 100644 index 00000000..6831d3ad --- /dev/null +++ b/.gitlab/issue_templates/Account Request.md @@ -0,0 +1,14 @@ +## 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 new file mode 100644 index 00000000..d125e22b --- /dev/null +++ b/.gitlab/issue_templates/Bug.md @@ -0,0 +1,36 @@ +- [ ] I have described the bug in complete detail in the + [Description](#description) section. +- [ ] I have specified steps in the [Reproduction](#reproduction) section. +- [ ] I have included any logs related to the bug in the + [Logs](#logs) section. +- [ ] I have included the versions which are affected in the + [Version(s)](#versions) section. + +### Description + +Describe the bug in full detail. + +### Reproduction + +Describe a specific set of actions that can be used to reproduce +this bug. + +### Logs + +If you have any logs relevent to the bug, include them here in +quoted or code blocks. + +### Version(s) + +In this section, please include a list of versions you have found +to be affected by this program. This can either come in the form +of `major.minor.patch` (if it affects a release tarball), or a +commit hash if the bug does not directly affect a release version. + +All development is done without modifying version displays in +aurweb's HTML render output. If you're testing locally, use the +commit on which you are experiencing the bug. If you have found +a bug which exists on live aur.archlinux.org, include the version +located at the bottom of the webpage. + +/label bug unconfirmed diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md new file mode 100644 index 00000000..c907adcd --- /dev/null +++ b/.gitlab/issue_templates/Feature.md @@ -0,0 +1,32 @@ +- [ ] 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. + +### Summary + +Fill this section out with a concise wording about the feature being +requested. + +Example: _A new `Tyrant` account type for users_. + +### Description + +Describe your feature in full detail. + +Example: _The `Tyrant` account type should be used to allow a user to be +tyrannical. When a user is a `Tyrant`, they should be able to assassinate +users due to not complying with their laws. Laws can be configured by updating +the Tyrant laws page at https://aur.archlinux.org/account/{username}/laws. +More specifics about laws._ + +### Blockers + +Include any blockers in a list. If there are no blockers, this section +should be omitted from the issue. + +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 new file mode 100644 index 00000000..950ec0c6 --- /dev/null +++ b/.gitlab/issue_templates/Feedback.md @@ -0,0 +1,58 @@ +**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 new file mode 100644 index 00000000..525c7eb8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +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 + hooks: + - <<: *flake8 + - <<: *isort + args: ['--check-only', '--diff'] + diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5bbfda1f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python - -python: 3.6 - -addons: - apt: - packages: - - bsdtar - - libarchive-dev - - libgpgme11-dev - - libprotobuf-dev - -install: - - curl https://codeload.github.com/libgit2/libgit2/tar.gz/v0.26.0 | tar -xz - - curl https://sources.archlinux.org/other/pacman/pacman-5.0.2.tar.gz | tar -xz - - curl https://git.archlinux.org/pyalpm.git/snapshot/pyalpm-0.8.1.tar.gz | tar -xz - - ( cd libgit2-0.26.0 && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && make && sudo make install ) - - ( cd pacman-5.0.2 && ./configure --prefix=/usr && make && sudo make install ) - - ( cd pyalpm-0.8.1 && python setup.py build && python setup.py install ) - - pip install mysql-connector-python-rf pygit2==0.26 srcinfo - - pip install bleach Markdown - -script: make -C test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d1b0da60 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + +Patches should be sent to the [aur-dev@lists.archlinux.org][1] mailing list. + +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 + +### Coding Guidelines + +1. All source modified or added within a patchset **must** maintain equivalent + or increased coverage by providing tests that use the functionality. + +2. Please keep your source within an 80 column width. + +Test patches that increase coverage in the codebase are always welcome. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..16e6514e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,42 @@ +FROM archlinux:base-devel + +VOLUME /root/.cache/pypoetry/cache +VOLUME /root/.cache/pypoetry/artifacts + +ENV PATH="/root/.poetry/bin:${PATH}" +ENV PYTHONPATH=/aurweb +ENV AUR_CONFIG=conf/config + +# Install system-wide dependencies. +COPY ./docker/scripts/install-deps.sh /install-deps.sh +RUN /install-deps.sh + +# Copy Docker scripts +COPY ./docker /docker +COPY ./docker/scripts/* /usr/local/bin/ + + +# Copy over all aurweb files. +COPY . /aurweb + +# Working directory is aurweb root @ /aurweb. +WORKDIR /aurweb + +# Copy initial config to conf/config. +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 + +# Compile asciidocs. +RUN make -C doc + +# Add our aur user. +RUN useradd -U -d /aurweb -c 'AUR User' aur + +# Setup some default system stuff. +RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime + +# Install translations. +RUN make -C po all install diff --git a/INSTALL b/INSTALL index 5a05b0ba..03459726 100644 --- a/INSTALL +++ b/INSTALL @@ -4,62 +4,135 @@ Setup on Arch Linux For testing aurweb patches before submission, you can use the instructions in TESTING for testing the web interface only. -Note that you can only do limited testing using the PHP built-in web server. -In particular, the cgit interface will be unusable as well as the ssh+git -interface. For a detailed description on how to setup a full aurweb server, +For a detailed description on how to setup a full aurweb server, read the instructions below. -1) Clone the aurweb project: +1) Clone the aurweb project and install it (via `python-poetry`): - $ cd /srv/http/ - $ git clone git://git.archlinux.org/aurweb.git + $ cd /srv/http/ + $ git clone git://git.archlinux.org/aurweb.git + $ 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: server { - listen 80; + # https is preferred and can be done easily with LetsEncrypt + # or self-CA signing. Users can still listen over 80 for plain + # http, for which the [options] disable_http_login used to toggle + # the authentication feature. + listen 443 ssl http2; server_name aur.local aur; - root /srv/http/aurweb/web/html; - index index.php; + # To enable SSL proxy properly, make sure gunicorn and friends + # are supporting forwarded headers over 127.0.0.1 or any if + # the asgi server is contacted by non-localhost hosts. + ssl_certificate /etc/ssl/certs/aur.cert.pem; + ssl_certificate_key /etc/ssl/private/aur.key.pem; - location ~ ^/[^/]+\.php($|/) { - fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock; - 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; + # 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; } - location ~ .* { - rewrite ^/(.*)$ /index.php/$1 last; + # 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; + 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 /srv/http/aurweb; + } + + # cgitrc.proto should be configured and located somewhere + # of your choosing. + location ~ ^/cgit { + include uwsgi_params; + rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=$1&$2 last; + uwsgi_modifier1 9; + uwsgi_param CGIT_CONFIG /srv/http/aurweb/conf/cgitrc.proto; + uwsgi_pass cgit; + } + + # Static archive assets. + location ~ \.gz$ { + types { application/gzip text/plain } + default_type text/plain; + add_header Content-Encoding gzip; + expires 5m; + } + + # For everything else, proxy the http request to (guni|uvi|hyper)corn. + # The ASGI server application should allow this request's IP to be + # forwarded via the headers used below. + # https://docs.gunicorn.org/en/stable/settings.html#forwarded-allow-ips + location / { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol ssl; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Ssl on; } } - Ensure to enable the pdo_mysql extension in php.ini. - 3) Optionally copy conf/config.defaults to /etc/aurweb/. Create or copy /etc/aurweb/config (this is expected to contain all configuration settings if the defaults file does not exist) and adjust the configuration (pay attention to disable_http_login, enable_maintenance and aur_location). -4) Install Python modules and dependencies: +4) Install system-wide dependencies: - # pacman -S python-mysql-connector python-pygit2 python-srcinfo python-sqlalchemy \ - python-bleach python-markdown python-alembic python-orjson - # python3 setup.py install + # pacman -S git gpgme cgit curl openssh uwsgi uwsgi-plugin-cgi \ + python-poetry -5) Create a new MySQL database and a user and import the aurweb SQL schema: - - $ python -m aurweb.initdb - -6) Create a new user: +5) Create a new user: # useradd -U -d /srv/http/aurweb -c 'AUR user' aur + # su - aur -7) Initialize the Git repository: +6a) Install Python dependencies via poetry: + + # Install the package and scripts as the aur user. + $ poetry install + +6b) Setup Services + +aurweb utilizes the following systemd services: +- mariadb +- redis (optional, requires [options] cache 'redis') +- `examples/aurweb.service` + +6c) Setup Cron + +Using [cronie](https://archlinux.org/packages/core/x86_64/cronie/): + + # su - aur + $ crontab -e + +The following crontab file uses every script meant to be run on an +interval: + + AUR_CONFIG='/etc/aurweb/config' + */5 * * * * bash -c 'poetry run aurweb-mkpkglists --extended' + */2 * * * * bash -c 'poetry run aurweb-aurblup' + */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' + +7) Create a new database and a user and import the aurweb SQL schema: + + $ poetry run python -m aurweb.initdb + +8) Initialize the Git repository: # mkdir /srv/http/aurweb/aur.git/ # cd /srv/http/aurweb/aur.git/ @@ -67,19 +140,26 @@ read the instructions below. # git config --local transfer.hideRefs '^refs/' # git config --local --add transfer.hideRefs '!refs/' # git config --local --add transfer.hideRefs '!HEAD' - # ln -s /usr/local/bin/aurweb-git-update hooks/update # chown -R aur . +Link to `aurweb-git-update` poetry wrapper provided at +`examples/aurweb-git-update.sh` which should be installed +somewhere as executable. + + # ln -s /path/to/aurweb-git-update.sh hooks/update + It is recommended to read doc/git-interface.txt for more information on the administration of the package Git repository. -8) Configure sshd(8) for the AUR. Add the following lines at the end of your - sshd_config(5) and restart the sshd. Note that OpenSSH 6.9 or newer is - needed! +9) Configure sshd(8) for the AUR. Add the following lines at the end of your + sshd_config(5) and restart the sshd. + +If using a virtualenv, copy `examples/aurweb-git-auth.sh` to a location +and call it below: Match User aur PasswordAuthentication no - AuthorizedKeysCommand /usr/local/bin/aurweb-git-auth "%t" "%k" + AuthorizedKeysCommand /path/to/aurweb-git-auth.sh "%t" "%k" AuthorizedKeysCommandUser aur AcceptEnv AUR_OVERWRITE @@ -98,8 +178,17 @@ read the instructions below. Sample systemd unit files for fcgiwrap can be found under conf/. -10) If you want memcache to cache MySQL data. +10) If you want Redis to cache data. - # pacman -S php-memcached + # pacman -S redis + # systemctl enable --now redis - And edit the configuration file to enabled memcache caching. + And edit the configuration file to enabled redis caching + (`[options] cache = redis`). + +11) Start `aurweb.service`. + +An example systemd unit has been included at `examples/aurweb.service`. +This unit can be used to manage the aurweb asgi backend. By default, +it is configured to use `poetry` as the `aur` user; this should be +configured as needed. diff --git a/LICENSES/starlette_exporter b/LICENSES/starlette_exporter new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSES/starlette_exporter @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index f7285a51..9ba64338 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,19 @@ Directory Layout * `aurweb`: aurweb Python modules, Git interface and maintenance scripts * `conf`: configuration and configuration templates +* `static`: static resource files +* `templates`: jinja2 template collection * `doc`: project documentation * `po`: translation files for strings in the aurweb interface * `schema`: schema for the SQL database * `test`: test suite and test cases * `upgrading`: instructions for upgrading setups from one release to another -* `web`: web interface for the AUR +* `web`: PHP-based web interface for the AUR Links ----- -* The repository is hosted at git://git.archlinux.org/aurweb.git -- see +* The repository is hosted at https://gitlab.archlinux.org/archlinux/aurweb -- see doc/CodingGuidelines for information on the patch submission process. * Bugs can (and should) be submitted to the aurweb bug tracker: @@ -45,4 +47,9 @@ Translations Translations are welcome via our Transifex project at https://www.transifex.com/lfleischer/aurweb; see `doc/i18n.txt` for details. -![Transifex](http://www.transifex.net/projects/p/aurweb/chart/image_png) +![Transifex](https://www.transifex.com/projects/p/aurweb/chart/image_png) + +Testing +------- + +See [test/README.md](test/README.md) for details on dependencies and testing. diff --git a/TESTING b/TESTING index 4a1e6f4c..776be2f4 100644 --- a/TESTING +++ b/TESTING @@ -5,32 +5,73 @@ 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 2) Install the necessary packages: - # pacman -S --needed php php-sqlite sqlite words fortune-mod \ - python python-sqlalchemy python-alembic + # pacman -S python-poetry - Ensure to enable the pdo_sqlite extension in php.ini. - -3) Copy conf/config.defaults to conf/config and adjust the configuration - (pay attention to disable_http_login, enable_maintenance and aur_location). - - Be sure to change backend to sqlite and name to the file location of your - created test database. - -4) Prepare the testing database: +4) Install the package/dependencies via `poetry`: $ cd /path/to/aurweb/ - $ python -m aurweb.initdb + $ poetry install - $ cd /path/to/aurweb/schema - $ ./gendummydata.py out.sql - $ sqlite3 path/to/aurweb.sqlite3 < out.sql +3) 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: -5) Run the PHP built-in web server: + $ sed -e "s;YOUR_AUR_ROOT;$PWD;g" conf/config.dev > conf/config - $ AUR_CONFIG='/path/to/aurweb/conf/config' php -S localhost:8080 -t /path/to/aurweb/web/html + 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: + + $ cd /path/to/aurweb/ + + $ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb + + $ poetry run schema/gendummydata.py dummy_data.sql + $ mysql -uaur -paur aurweb < dummy_data.sql + +5) 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 diff --git a/aurweb/asgi.py b/aurweb/asgi.py new file mode 100644 index 00000000..2dd546aa --- /dev/null +++ b/aurweb/asgi.py @@ -0,0 +1,259 @@ +import hashlib +import http +import io +import os +import re +import sys +import traceback +import typing + +from urllib.parse import quote_plus + +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 starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.middleware.authentication import AuthenticationMiddleware +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.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.scripts import notify +from aurweb.templates import make_context, render_template + +logger = logging.get_logger(__name__) + +# Setup the FastAPI app. +app = FastAPI() + +# Instrument routes with the prometheus-fastapi-instrumentator +# library with custom collectors and expose /metrics. +instrumentator().add(prometheus.http_api_requests_total()) +instrumentator().add(prometheus.http_requests_total()) +instrumentator().instrument(app) + + +@app.on_event("startup") +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 + # using starlette.testclient.TestClient. Looking around in regards + # to the recursion error has really not recommended a course of action + # other than increasing the recursion limit. For now, that is how + # we handle the issue: an optional TEST_RECURSION_LIMIT env var + # 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)) + 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())}") + + session_secret = aurweb.config.get("fastapi", "session_secret") + if not session_secret: + raise Exception("[fastapi] session_secret must not be empty") + + 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) + + # 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. + + :param request: FastAPI Request + :return: Rendered 500.html template with status_code 500 + """ + context = make_context(request, "Internal Server Error") + + # Print out the exception via `traceback` and store the value + # into the `traceback` context variable. + tb_io = io.StringIO() + traceback.print_exc(file=tb_io) + tb = tb_io.getvalue() + context["traceback"] = tb + + # Produce a SHA1 hash of the traceback string. + tb_hash = hashlib.sha1(tb.encode()).hexdigest() + + # Use the first 7 characters of the sha1 for the traceback id. + # We will use this to log and include in the notification. + tb_id = tb_hash[:7] + + redis = redis_connection() + pipe = redis.pipeline() + key = f"tb:{tb_hash}" + pipe.get(key) + retval, = pipe.execute() + if not retval: + # Expire in one hour; this is just done to make sure we + # don't infinitely store these values, but reduce the number + # of automated reports (notification below). At this time of + # writing, unexpected exceptions are not common, thus this + # will not produce a large memory footprint in redis. + pipe.set(key, tb) + pipe.expire(key, 3600) + pipe.execute() + + # Send out notification about it. + notif = notify.ServerErrorNotification( + tb_id, context.get("version"), context.get("utcnow")) + notif.send() + + retval = tb + else: + retval = retval.decode() + + # Log details about the exception traceback. + logger.error(f"FATAL[{tb_id}]: An unexpected exception has occurred.") + logger.error(retval) + + 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. """ + phrase = http.HTTPStatus(exc.status_code).phrase + context = make_context(request, phrase) + context["exc"] = exc + context["phrase"] = phrase + + # Additional context for some exceptions. + 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: + try: + pkgbase = get_pkg_or_base(matches.group(1)) + context = pkgbaseutil.make_context(request, pkgbase) + except HTTPException: + pass + + try: + 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) + + +@app.middleware("http") +async def add_security_headers(request: Request, call_next: typing.Callable): + """ This middleware adds the CSP, XCTO, XFO and RP security + headers to the HTTP response associated with request. + + CSP: Content-Security-Policy + XCTO: X-Content-Type-Options + RP: Referrer-Policy + XFO: X-Frame-Options + """ + try: + response = await util.error_or_result(call_next, request) + except Exception as exc: + return await internal_server_error(request, exc) + + # 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'" + response.headers["Content-Security-Policy"] = csp + + # Add XTCO header. + xcto = "nosniff" + response.headers["X-Content-Type-Options"] = xcto + + # Add Referrer Policy header. + rp = "same-origin" + response.headers["Referrer-Policy"] = rp + + # Add X-Frame-Options header. + xfo = "SAMEORIGIN" + response.headers["X-Frame-Options"] = xfo + + return response + + +@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. """ + 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)) + + return await util.error_or_result(call_next, request) + + +@app.middleware("http") +async def id_redirect_middleware(request: Request, call_next: typing.Callable): + id = request.query_params.get("id") + + if id is not None: + # Preserve query string. + qs = [] + 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) + + path = request.url.path.rstrip('/') + return RedirectResponse(f"{path}/{id}{qs}") + + return await util.error_or_result(call_next, request) diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py new file mode 100644 index 00000000..c4f433c0 --- /dev/null +++ b/aurweb/auth/__init__.py @@ -0,0 +1,226 @@ +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. """ + + def filter(self, *args): + return StubQuery() + + def scalar(self): + return 0 + + +class AnonymousUser: + """ A stubbed User class used when an unauthenticated User + makes a request against FastAPI. """ + # Stub attributes used to mimic a real user. + ID = 0 + + class AccountType: + """ 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. """ + ID = 0 + AccountType = "Anonymous" + + # AccountTypeID == AccountType.ID; assign a stubbed column. + AccountTypeID = AccountType.ID + + LangPreference = aurweb.config.get("options", "default_lang") + Timezone = aurweb.config.get("options", "default_timezone") + + Suspended = 0 + InactivityTS = 0 + + # A stub ssh_pub_key relationship. + ssh_pub_key = None + + # Add stubbed relationship backrefs. + notifications = StubQuery() + package_votes = StubQuery() + + # A nonce attribute, needed for all browser sessions; set in __init__. + nonce = None + + def __init__(self): + self.nonce = util.make_nonce() + + @staticmethod + def is_authenticated(): + return False + + @staticmethod + def is_trusted_user(): + return False + + @staticmethod + def is_developer(): + return False + + @staticmethod + def is_elevated(): + return False + + @staticmethod + def has_credential(credential, **kwargs): + return False + + @staticmethod + def voted_for(package): + return False + + @staticmethod + def notified(package): + return False + + +class BasicAuthBackend(AuthenticationBackend): + async def authenticate(self, conn: HTTPConnection): + unauthenticated = (None, AnonymousUser()) + sid = conn.cookies.get("AURSID") + if not sid: + return unauthenticated + + timeout = aurweb.config.getint("options", "login_timeout") + remembered = ("AURREMEMBER" in conn.cookies + and bool(conn.cookies.get("AURREMEMBER"))) + if remembered: + timeout = aurweb.config.getint("options", + "persistent_cookie_timeout") + + # If no session with sid and a LastUpdateTS now or later exists. + now_ts = time.utcnow() + record = db.query(Session).filter(Session.SessionID == sid).first() + if not record: + return unauthenticated + elif record.LastUpdateTS < (now_ts - timeout): + with db.begin(): + db.delete_all([record]) + return unauthenticated + + # 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.nonce = util.make_nonce() + user.authenticated = True + + return (AuthCredentials(["authenticated"]), user) + + +def _auth_required(auth_goal: bool = True): + """ + Enforce a user's authentication status, bringing them to the login page + or homepage if their authentication status does not match the goal. + + NOTE: This function should not need to be used in downstream code. + See `requires_auth` and `requires_guest` for decorators meant to be + used on routes (they're a bit more implicitly understandable). + + :param auth_goal: Whether authentication is required or entirely disallowed + for a user to perform this request. + :return: Return the FastAPI function this decorator wraps. + """ + + def decorator(func): + @functools.wraps(func) + async def wrapper(request, *args, **kwargs): + if request.user.is_authenticated() == auth_goal: + return await func(request, *args, **kwargs) + + url = "/" + if auth_goal is False: + return RedirectResponse(url, status_code=int(HTTPStatus.SEE_OTHER)) + + # Use the request path when the user can visit a page directly but + # is not authenticated and use the Referer header if visiting the + # 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")): + 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:] + 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. """ + + @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. """ + + @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 + that a user belongs to one of the types defined in one_of. + + This decorator should be run after an @auth_required(True) is + dictated. + + - Example code: + + @router.get('/some_route') + @auth_required(True) + @account_type_required({"Trusted User", "Trusted User & Developer"}) + async def some_route(request: fastapi.Request): + return Response() + + :param one_of: A set consisting of strings to match against AccountType. + :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) + } + + 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 await func(request, *args, **kwargs) + return wrapper + return decorator diff --git a/aurweb/auth/creds.py b/aurweb/auth/creds.py new file mode 100644 index 00000000..100aad8c --- /dev/null +++ b/aurweb/auth/creds.py @@ -0,0 +1,76 @@ +from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.user import User + +ACCOUNT_CHANGE_TYPE = 1 +ACCOUNT_EDIT = 2 +ACCOUNT_EDIT_DEV = 3 +ACCOUNT_LAST_LOGIN = 4 +ACCOUNT_SEARCH = 5 +ACCOUNT_LIST_COMMENTS = 28 +COMMENT_DELETE = 6 +COMMENT_UNDELETE = 27 +COMMENT_VIEW_DELETED = 22 +COMMENT_EDIT = 25 +COMMENT_PIN = 26 +PKGBASE_ADOPT = 7 +PKGBASE_SET_KEYWORDS = 8 +PKGBASE_DELETE = 9 +PKGBASE_DISOWN = 10 +PKGBASE_EDIT_COMAINTAINERS = 24 +PKGBASE_FLAG = 11 +PKGBASE_LIST_VOTERS = 12 +PKGBASE_NOTIFY = 13 +PKGBASE_UNFLAG = 15 +PKGBASE_VOTE = 16 +PKGREQ_FILE = 23 +PKGREQ_CLOSE = 17 +PKGREQ_LIST = 18 +TU_ADD_VOTE = 19 +TU_LIST_VOTES = 20 +TU_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]) + +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, + ACCOUNT_EDIT_DEV: developer, + PKGBASE_MERGE: trusted_user_or_dev, +} + + +def has_credential(user: User, + credential: int, + approved_users: list = tuple()): + + if user in approved_users: + return True + return user.AccountTypeID in cred_filters[credential] diff --git a/aurweb/benchmark.py b/aurweb/benchmark.py new file mode 100644 index 00000000..7086fb08 --- /dev/null +++ b/aurweb/benchmark.py @@ -0,0 +1,21 @@ +from datetime import datetime + + +class Benchmark: + def __init__(self): + self.start() + + def _timestamp(self) -> float: + """ Generate a timestamp. """ + return float(datetime.utcnow().timestamp()) + + def start(self) -> int: + """ Start a benchmark. """ + self.current = self._timestamp() + return self.current + + def end(self): + """ 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 new file mode 100644 index 00000000..697473b8 --- /dev/null +++ b/aurweb/cache.py @@ -0,0 +1,20 @@ +from redis import Redis +from sqlalchemy import orm + + +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. + + :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) + if result is None: + redis.set(key, (result := int(query.count()))) + if expire: + redis.expire(key, expire) + return int(result) diff --git a/aurweb/captcha.py b/aurweb/captcha.py new file mode 100644 index 00000000..34d99e53 --- /dev/null +++ b/aurweb/captcha.py @@ -0,0 +1,57 @@ +""" This module consists of aurweb's CAPTCHA utility functions and filters. """ +import hashlib + +from jinja2 import pass_context + +from aurweb.db import query +from aurweb.models import User +from aurweb.templates import register_filter + + +def get_captcha_salts(): + """ Produce salts based on the current user count. """ + count = query(User).count() + salts = [] + for i in range(0, 6): + salts.append(f"aurweb-{count - i}") + return salts + + +def get_captcha_token(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. """ + 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. """ + text = r""" + .--. Pacman v%s.%s.%s - libalpm v%s.%s.%s +/ _.-' .-. .-. .-. Copyright (C) %s-%s Pacman Development Team +\ '-. '-' '-' '-' Copyright (C) %s-%s Judd Vinet + '--' + This program may be freely redistributed under + the terms of the GNU General Public License. +""" % 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. """ + salts = get_captcha_salts() + return salts[0] + + +@register_filter("captcha_cmdline") +@pass_context +def captcha_cmdline_filter(context, 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 52ec461e..9ab9ae45 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -1,6 +1,13 @@ 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.1" + _parser = None @@ -12,6 +19,7 @@ def _get_parser(): defaults = os.environ.get('AUR_CONFIG_DEFAULTS', path + '.defaults') _parser = configparser.RawConfigParser() + _parser.optionxform = lambda option: option if os.path.isfile(defaults): with open(defaults) as f: _parser.read_file(f) @@ -20,6 +28,17 @@ def _get_parser(): return _parser +def rehash(): + """ Globally rehash the configuration parser. """ + global _parser + _parser = None + _get_parser() + + +def get_with_fallback(section, option, fallback): + return _get_parser().get(section, option, fallback=fallback) + + def get(section, option): return _get_parser().get(section, option) @@ -28,5 +47,25 @@ def getboolean(section, option): return _get_parser().getboolean(section, option) -def getint(section, option): - return _get_parser().getint(section, option) +def getint(section, option, fallback=None): + return _get_parser().getint(section, option, fallback=fallback) + + +def get_section(section): + if section in _get_parser().sections(): + return _get_parser()[section] + + +def unset_option(section: str, option: str) -> None: + _get_parser().remove_option(section, option) + + +def set_option(section: str, option: str, value: Any) -> None: + _get_parser().set(section, option, value) + return value + + +def save() -> None: + aur_config = os.environ.get("AUR_CONFIG", "/etc/aurweb/config") + with open(aur_config, "w") as fp: + _get_parser().write(fp) diff --git a/aurweb/cookies.py b/aurweb/cookies.py new file mode 100644 index 00000000..442a4c0a --- /dev/null +++ b/aurweb/cookies.py @@ -0,0 +1,68 @@ +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. + + When options.disable_http_login is True, "strict" is returned. Otherwise, + "lax" is returned. + + :returns "strict" if options.disable_http_login else "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 diff --git a/aurweb/db.py b/aurweb/db.py index 645a1c6a..4c53730a 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,35 +1,205 @@ -try: - import mysql.connector -except ImportError: - pass +import functools +import hashlib +import math +import os +import re -try: - import sqlite3 -except ImportError: - pass +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") -def get_sqlalchemy_url(): +def make_random_value(table: str, column: str, length: int): + """ Generate a unique, random value for a string column in a table. + + :return: A unique string that is not in the database """ - Build an SQLAlchemy for use with create_engine based on the aurweb configuration. + string = aurweb.util.make_random_string(length) + while query(table).filter(column == string).first(): + string = aurweb.util.make_random_string(length) + return string + + +def test_name() -> str: """ - import sqlalchemy + Return the unhashed database name. + + The unhashed database name is determined (lower = higher priority) by: + ------------------------------------------- + 1. {test_suite} portion of PYTEST_CURRENT_TEST + 2. aurweb.config.get("database", "name") + + During `pytest` runs, the PYTEST_CURRENT_TEST environment variable + is set to the current test in the format `{test_suite}::{test_func}`. + + This allows tests to use a suite-specific database for its runs, + which decouples database state from test suites. + + :return: Unhashed database name + """ + db = os.environ.get("PYTEST_CURRENT_TEST", + aurweb.config.get("database", "name")) + return db.split(":")[0] + + +def name() -> str: + """ + Return sanitized database name that can be used for tests or production. + + If test_name() starts with "test/", the database name is SHA-1 hashed, + prefixed with 'db', and returned. Otherwise, test_name() is passed + through and not hashed at all. + + :return: SHA1-hashed database name prefixed with 'db' + """ + dbname = test_name() + if not dbname.startswith("test/"): + return dbname + sha1 = hashlib.sha1(dbname.encode()).hexdigest() + return "db" + sha1 + + +# Module-private global memo used to store SQLAlchemy sessions. +_sessions = dict() + + +def get_session(engine: Engine = None) -> Session: + """ Return aurweb.db's global session. """ + dbname = name() + + global _sessions + if dbname not in _sessions: + + if not engine: # pragma: no cover + engine = get_engine() + + Session = scoped_session( + sessionmaker(autocommit=True, autoflush=False, bind=engine)) + _sessions[dbname] = Session() + + return _sessions.get(dbname) + + +def pop_session(dbname: str) -> None: + """ + Pop a Session out of the private _sessions memo. + + :param dbname: Database name + :raises KeyError: When `dbname` does not exist in the memo + """ + global _sessions + _sessions.pop(dbname) + + +def refresh(model: Base) -> Base: + """ Refresh the session's knowledge of `model`. """ + get_session().refresh(model) + return model + + +def query(Model: Base, *args, **kwargs) -> Query: + """ + Perform an ORM query against the database session. + + This method also runs Query.filter on the resulting model + query with *args and **kwargs. + + :param Model: Declarative ORM class + """ + return get_session().query(Model).filter(*args, **kwargs) + + +def create(Model: Base, *args, **kwargs) -> Base: + """ + Create a record and add() it to the database session. + + :param Model: Declarative ORM class + :return: Model instance + """ + instance = Model(*args, **kwargs) + return add(instance) + + +def delete(model: Base) -> None: + """ + Delete a set of records found by Query.filter(*args, **kwargs). + + :param Model: Declarative ORM class + """ + get_session().delete(model) + + +def delete_all(iterable: Iterable) -> None: + """ Delete each instance found in `iterable`. """ + session_ = get_session() + aurweb.util.apply_all(iterable, session_.delete) + + +def rollback() -> None: + """ Rollback the database session. """ + get_session().rollback() + + +def add(model: Base) -> Base: + """ Add `model` to the database session. """ + get_session().add(model) + return model + + +def begin() -> SessionTransaction: + """ Begin an SQLAlchemy SessionTransaction. """ + return get_session().begin() + + +def get_sqlalchemy_url() -> URL: + """ + Build an SQLAlchemy URL for use with create_engine. + + :return: sqlalchemy.engine.url.URL + """ + constructor = URL + + 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': - return sqlalchemy.engine.url.URL( - 'mysql+mysqlconnector', + param_query = {} + port = aurweb.config.get_with_fallback("database", "port", None) + if not port: + 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('database', 'password'), + password=aurweb.config.get_with_fallback('database', 'password', + fallback=None), host=aurweb.config.get('database', 'host'), - database=aurweb.config.get('database', 'name'), - query={ - 'unix_socket': aurweb.config.get('database', 'socket'), - }, + database=name(), + port=port, + query=param_query ) elif aur_db_backend == 'sqlite': - return sqlalchemy.engine.url.URL( + return constructor( 'sqlite', database=aurweb.config.get('database', 'name'), ) @@ -37,34 +207,114 @@ def get_sqlalchemy_url(): raise ValueError('unsupported database backend') -class Connection: +def sqlite_regexp(regex, item) -> bool: # pragma: no cover + """ Method which mimics SQL's REGEXP for SQLite. """ + return bool(re.search(regex, str(item))) + + +def setup_sqlite(engine: Engine) -> None: # pragma: no cover + """ Perform setup for an SQLite engine. """ + @event.listens_for(engine, "connect") + def do_begin(conn, record): + create_deterministic_function = functools.partial( + conn.create_function, + deterministic=True + ) + create_deterministic_function("REGEXP", 2, sqlite_regexp) + + +# Module-private global memo used to store SQLAlchemy engines. +_engines = dict() + + +def get_engine(dbname: str = None, echo: bool = False) -> Engine: + """ + Return the SQLAlchemy engine for `dbname`. + + The engine is created on the first call to get_engine and then stored in the + `engine` global variable for the next calls. + + :param dbname: Database name (default: aurweb.db.name()) + :param echo: Flag passed through to sqlalchemy.create_engine + :return: SQLAlchemy Engine instance + """ + if not dbname: + dbname = name() + + global _engines + if dbname not in _engines: + db_backend = aurweb.config.get("database", "backend") + connect_args = dict() + + is_sqlite = bool(db_backend == "sqlite") + 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) + + if is_sqlite: # pragma: no cover + setup_sqlite(_engines.get(dbname)) + + return _engines.get(dbname) + + +def pop_engine(dbname: str) -> None: + """ + Pop an Engine out of the private _engines memo. + + :param dbname: Database name + :raises KeyError: When `dbname` does not exist in the memo + """ + global _engines + _engines.pop(dbname) + + +def kill_engine() -> None: + """ Close the current session and dispose of the engine. """ + dbname = name() + + session = get_session() + session.close() + pop_session(dbname) + + engine = get_engine() + engine.dispose() + pop_engine(dbname) + + +def connect(): + """ + Return an SQLAlchemy connection. Connections are usually pooled. See + . + + Since SQLAlchemy connections are context managers too, you should use it + with Python’s `with` operator, or with FastAPI’s dependency injection. + """ + return get_engine().connect() + + +class ConnectionExecutor: _conn = None _paramstyle = None - def __init__(self): - aur_db_backend = aurweb.config.get('database', 'backend') - - if aur_db_backend == 'mysql': - aur_db_host = aurweb.config.get('database', 'host') - aur_db_name = aurweb.config.get('database', 'name') - aur_db_user = aurweb.config.get('database', 'user') - aur_db_pass = aurweb.config.get('database', 'password') - aur_db_socket = aurweb.config.get('database', 'socket') - self._conn = mysql.connector.connect(host=aur_db_host, - user=aur_db_user, - passwd=aur_db_pass, - db=aur_db_name, - unix_socket=aur_db_socket, - buffered=True) - self._paramstyle = mysql.connector.paramstyle - elif aur_db_backend == 'sqlite': - aur_db_name = aurweb.config.get('database', 'name') - self._conn = sqlite3.connect(aur_db_name) + def __init__(self, conn, backend=aurweb.config.get("database", "backend")): + self._conn = conn + if backend == "mysql": + self._paramstyle = "format" + elif backend == "sqlite": + import sqlite3 self._paramstyle = sqlite3.paramstyle - else: - raise ValueError('unsupported database backend') - def execute(self, query, params=()): + def paramstyle(self): + return self._paramstyle + + 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': @@ -82,3 +332,45 @@ class Connection: def close(self): self._conn.close() + + +class Connection: + _executor = None + _conn = None + + def __init__(self): + 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_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 + # TODO: SQLite support has been removed in FastAPI. It remains + # here to fund its support for PHP until it is removed. + import sqlite3 + 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') + + self._conn = ConnectionExecutor(self._conn, aur_db_backend) + + def execute(self, query, params=()): + return self._conn.execute(query, params) + + def commit(self): + self._conn.commit() + + def close(self): + self._conn.close() diff --git a/aurweb/defaults.py b/aurweb/defaults.py new file mode 100644 index 00000000..51072e8f --- /dev/null +++ b/aurweb/defaults.py @@ -0,0 +1,21 @@ +""" Constant default values centralized in one place. """ + +# Default [O]ffset +O = 0 + +# Default [P]er [P]age +PP = 50 + +# A whitelist of valid PP values +PP_WHITELIST = {50, 100, 250} + +# Default `by` parameter for RPC search. +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 not in PP_WHITELIST: + return PP + return per_page diff --git a/aurweb/exceptions.py b/aurweb/exceptions.py index 62015284..1c45b7f3 100644 --- a/aurweb/exceptions.py +++ b/aurweb/exceptions.py @@ -1,3 +1,6 @@ +from typing import Any + + class AurwebException(Exception): pass @@ -73,3 +76,17 @@ class NotVotedException(AurwebException): class InvalidArgumentsException(AurwebException): def __init__(self, msg): super(InvalidArgumentsException, self).__init__(msg) + + +class RPCError(AurwebException): + pass + + +class ValidationError(AurwebException): + def __init__(self, data: Any, *args, **kwargs): + super().__init__(*args, **kwargs) + self.data = data + + +class InvariantError(AurwebException): + pass diff --git a/aurweb/filters.py b/aurweb/filters.py new file mode 100644 index 00000000..9b731501 --- /dev/null +++ b/aurweb/filters.py @@ -0,0 +1,150 @@ +import copy +import math + +from datetime import datetime +from typing import Any, Dict +from urllib.parse import quote_plus, urlencode +from zoneinfo import ZoneInfo + +import fastapi +import paginate + +from jinja2 import pass_context + +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: + page = int(page) # Make sure this is an int. + + pp = context.get("PP", 50) + + # Setup a local query string dict, optionally passed by caller. + q = context.get("q", dict()) + + search_by = context.get("SeB", None) + if search_by: + q["SeB"] = search_by + + sort_by = context.get("SB", None) + if sort_by: + q["SB"] = sort_by + + def create_url(page: int): + nonlocal q + offset = max(page * pp - pp, 0) + qs = to_qs(extend_query(q, ["O", offset])) + 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) + + return pager.pager( + link_attr={"class": "page"}, + curpage_attr={"class": "page"}, + separator=" ", + format="$link_first $link_previous ~5~ $link_next $link_last", + symbol_first="« First", + symbol_previous="‹ Previous", + symbol_next="Next ›", + symbol_last="Last »") + + +@register_function("config_getint") +def config_getint(section: str, key: str) -> int: + return config.getint(section, key) + + +@register_function("round") +def do_round(f: float) -> int: + return round(f) + + +@register_filter("tr") +@pass_context +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. + + Example: + {{ some_integer | tn("singular %d", "plural %d") }} + + :param context: Response context + :param count: The number used to decide singular or plural state + :param singular: The singular translation + :param plural: The plural translation + :return: Translated string + """ + gettext = l10n.get_raw_translator_for_request(context.get("request")) + return gettext.ngettext(singular, plural, count) + + +@register_filter("dt") +def timestamp_to_datetime(timestamp: int): + return datetime.utcfromtimestamp(int(timestamp)) + + +@register_filter("as_timezone") +def as_timezone(dt: datetime, timezone: str): + return dt.astimezone(tz=ZoneInfo(timezone)) + + +@register_filter("extend_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 + return q + + +@register_filter("urlencode") +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() + + +@register_filter("number_format") +def number_format(value: float, places: int): + """ 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: + base = aurweb.config.get("options", "aur_location") + return f"{base}/account/{user.Username}" + + +@register_filter("quote_plus") +def _quote_plus(*args, **kwargs) -> str: + return quote_plus(*args, **kwargs) + + +@register_filter("ceil") +def ceil(*args, **kwargs) -> int: + return math.ceil(*args, **kwargs) diff --git a/aurweb/git/auth.py b/aurweb/git/auth.py index 3b1e485f..abecd276 100755 --- a/aurweb/git/auth.py +++ b/aurweb/git/auth.py @@ -1,8 +1,7 @@ #!/usr/bin/env python3 -import os -import shlex import re +import shlex import sys import aurweb.config diff --git a/aurweb/git/serve.py b/aurweb/git/serve.py index 64d51b9e..b91f1a13 100755 --- a/aurweb/git/serve.py +++ b/aurweb/git/serve.py @@ -175,11 +175,11 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged): 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() @@ -268,7 +268,7 @@ def pkgbase_disown(pkgbase, user, privileged): cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user]) userid = cur.fetchone()[0] if userid == 0: - raise aurweb.exceptions.InvalidUserException(user) + raise aurweb.exceptions.InvalidUserException(user) subprocess.Popen((notify_cmd, 'disown', str(userid), str(pkgbase_id))) @@ -472,7 +472,7 @@ def checkarg(cmdargv, *argdesc): checkarg_atmost(cmdargv, *argdesc) -def serve(action, cmdargv, user, privileged, remote_addr): +def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901 if enable_maintenance: if remote_addr not in maintenance_exc: raise aurweb.exceptions.MaintenanceException diff --git a/aurweb/git/update.py b/aurweb/git/update.py index 39128f8b..2424bf6c 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -1,12 +1,12 @@ #!/usr/bin/env python3 import os -import pygit2 import re import subprocess import sys import time +import pygit2 import srcinfo.parse import srcinfo.utils @@ -75,7 +75,7 @@ def create_pkgbase(conn, pkgbase, user): return pkgbase_id -def save_metadata(metadata, conn, 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 " @@ -232,7 +232,7 @@ def die_commit(msg, commit): exit(1) -def main(): +def main(): # noqa: C901 repo = pygit2.Repository(repo_path) user = os.environ.get("AUR_USER") @@ -303,7 +303,11 @@ def main(): error['line'], err)) exit(1) - metadata_pkgbase = metadata['pkgbase'] + 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)) diff --git a/aurweb/initdb.py b/aurweb/initdb.py index 91777f7e..a4a9f621 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -1,10 +1,11 @@ -import aurweb.db -import aurweb.schema +import argparse import alembic.command import alembic.config -import argparse -import sqlalchemy + +import aurweb.db +import aurweb.logging +import aurweb.schema def feed_initial_data(conn): @@ -33,17 +34,21 @@ def feed_initial_data(conn): def run(args): + aurweb.config.rehash() + # Ensure Alembic is fine before we do the real work, in order not to fail at # 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.attributes["configure_logger"] = False - engine = sqlalchemy.create_engine(aurweb.db.get_sqlalchemy_url(), - echo=(args.verbose >= 1)) + engine = aurweb.db.get_engine(echo=(args.verbose >= 1)) aurweb.schema.metadata.create_all(engine) - feed_initial_data(engine.connect()) + conn = engine.connect() + feed_initial_data(conn) + conn.close() if args.use_alembic: alembic.command.stamp(alembic_config, 'head') diff --git a/aurweb/l10n.py b/aurweb/l10n.py index a7c0103e..f3bbc1da 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -1,19 +1,86 @@ 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": "正體中文" +}) + class Translator: def __init__(self): self._localedir = aurweb.config.get('options', 'localedir') self._translator = {} - def translate(self, s, lang): - if lang == 'en': - return s + def get_translator(self, lang: str): if lang not in self._translator: self._translator[lang] = gettext.translation("aurweb", self._localedir, - languages=[lang]) - self._translator[lang].install() - return _(s) + languages=[lang], + fallback=True) + return self._translator.get(lang) + + def translate(self, s: str, lang: str): + return self.get_translator(lang).gettext(s) + + +# Global translator object. +translator = Translator() + + +def get_request_language(request: Request): + if request.user.is_authenticated(): + return request.user.LangPreference + default_lang = aurweb.config.get("options", "default_lang") + return request.cookies.get("AURLANG", default_lang) + + +def get_raw_translator_for_request(request: Request): + lang = get_request_language(request) + return translator.get_translator(lang) + + +def get_translator_for_request(request: Request): + """ + Determine the preferred language from a FastAPI request object and build a + translator function for it. + """ + lang = get_request_language(request) + + def translate(message): + return translator.translate(message, lang) + + return translate diff --git a/aurweb/logging.py b/aurweb/logging.py new file mode 100644 index 00000000..116421e4 --- /dev/null +++ b/aurweb/logging.py @@ -0,0 +1,26 @@ +import logging +import logging.config +import os + +import aurweb.config + +# For testing, users should set LOG_CONFIG=logging.test.conf +# We test against various debug log output. +aurwebdir = aurweb.config.get("options", "aurwebdir") +log_config = os.environ.get("LOG_CONFIG", "logging.conf") +config_path = os.path.join(aurwebdir, log_config) + +logging.config.fileConfig(config_path, disable_existing_loggers=False) +logging.getLogger("root").addHandler(logging.NullHandler()) + + +def get_logger(name: str) -> logging.Logger: + """ 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. + + :param name: Logger name; typically `__name__` + :returns: name's logging.Logger + """ + + return logging.getLogger(name) diff --git a/aurweb/models/__init__.py b/aurweb/models/__init__.py new file mode 100644 index 00000000..a06077ad --- /dev/null +++ b/aurweb/models/__init__.py @@ -0,0 +1,31 @@ +""" 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 +from .ban import Ban # noqa: F401 +from .dependency_type import DependencyType # noqa: F401 +from .group import Group # noqa: F401 +from .license import License # noqa: F401 +from .official_provider import OfficialProvider # noqa: F401 +from .package import Package # noqa: F401 +from .package_base import PackageBase # noqa: F401 +from .package_blacklist import PackageBlacklist # noqa: F401 +from .package_comaintainer import PackageComaintainer # noqa: F401 +from .package_comment import PackageComment # noqa: F401 +from .package_dependency import PackageDependency # noqa: F401 +from .package_group import PackageGroup # noqa: F401 +from .package_keyword import PackageKeyword # noqa: F401 +from .package_license import PackageLicense # noqa: F401 +from .package_notification import PackageNotification # noqa: F401 +from .package_relation import PackageRelation # noqa: F401 +from .package_request import PackageRequest # noqa: F401 +from .package_source import PackageSource # noqa: F401 +from .package_vote import PackageVote # noqa: F401 +from .relation_type import RelationType # noqa: F401 +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 diff --git a/aurweb/models/accepted_term.py b/aurweb/models/accepted_term.py new file mode 100644 index 00000000..0f9b187e --- /dev/null +++ b/aurweb/models/accepted_term.py @@ -0,0 +1,36 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.term import Term as _Term +from aurweb.models.user import User as _User + + +class AcceptedTerm(Base): + __table__ = schema.AcceptedTerms + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.TermsID]} + + User = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.User and not self.UsersID: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="AcceptedTerms.UserID", + params=("NULL")) + + if not self.Term and not self.TermsID: + raise IntegrityError( + statement="Foreign key TermID cannot be null.", + orig="AcceptedTerms.TermID", + params=("NULL")) diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py new file mode 100644 index 00000000..a849df02 --- /dev/null +++ b/aurweb/models/account_type.py @@ -0,0 +1,40 @@ +from aurweb import schema +from aurweb.models.declarative import Base + +USER = "User" +TRUSTED_USER = "Trusted User" +DEVELOPER = "Developer" +TRUSTED_USER_AND_DEV = "Trusted User & Developer" + +USER_ID = 1 +TRUSTED_USER_ID = 2 +DEVELOPER_ID = 3 +TRUSTED_USER_AND_DEV_ID = 4 + +# Map string constants to integer constants. +ACCOUNT_TYPE_ID = { + USER: USER_ID, + TRUSTED_USER: TRUSTED_USER_ID, + DEVELOPER: DEVELOPER_ID, + TRUSTED_USER_AND_DEV: TRUSTED_USER_AND_DEV_ID +} + +# Reversed ACCOUNT_TYPE_ID mapping. +ACCOUNT_TYPE_NAME = {v: k for k, v in ACCOUNT_TYPE_ID.items()} + + +class AccountType(Base): + """ An ORM model of a single AccountTypes record. """ + __table__ = schema.AccountTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, **kwargs): + self.AccountType = kwargs.pop("AccountType") + + def __str__(self): + return str(self.AccountType) + + def __repr__(self): + return "" % ( + self.ID, str(self)) diff --git a/aurweb/models/api_rate_limit.py b/aurweb/models/api_rate_limit.py new file mode 100644 index 00000000..19b656df --- /dev/null +++ b/aurweb/models/api_rate_limit.py @@ -0,0 +1,25 @@ +from sqlalchemy.exc import IntegrityError + +from aurweb import schema +from aurweb.models.declarative import Base + + +class ApiRateLimit(Base): + __table__ = schema.ApiRateLimit + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.IP]} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if self.Requests is None: + raise IntegrityError( + statement="Column Requests cannot be null.", + orig="ApiRateLimit.Requests", + params=("NULL")) + + if self.WindowStart is None: + raise IntegrityError( + statement="Column WindowStart cannot be null.", + orig="ApiRateLimit.WindowStart", + params=("NULL")) diff --git a/aurweb/models/ban.py b/aurweb/models/ban.py new file mode 100644 index 00000000..0fcb6d2e --- /dev/null +++ b/aurweb/models/ban.py @@ -0,0 +1,19 @@ +from fastapi import Request + +from aurweb import db, schema +from aurweb.models.declarative import Base + + +class Ban(Base): + __table__ = schema.Bans + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.IPAddress]} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +def is_banned(request: Request): + ip = request.client.host + 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 new file mode 100644 index 00000000..20ddd20c --- /dev/null +++ b/aurweb/models/declarative.py @@ -0,0 +1,36 @@ +import json + +from sqlalchemy.ext.declarative import declarative_base + +from aurweb import util + + +def to_dict(model): + 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) + + +Base = declarative_base() + +# Setup __table_args__ applicable to every table. +Base.__table_args__ = { + "autoload": False, + "extend_existing": True +} + +# Setup Base.as_dict and Base.json. +# +# With this, declarative models can use .as_dict() or .json() +# at any time to produce a dict and json out of table columns. +# +Base.as_dict = to_dict +Base.json = to_json diff --git a/aurweb/models/dependency_type.py b/aurweb/models/dependency_type.py new file mode 100644 index 00000000..98418802 --- /dev/null +++ b/aurweb/models/dependency_type.py @@ -0,0 +1,21 @@ +from aurweb import schema +from aurweb.models.declarative import Base + +DEPENDS = "depends" +MAKEDEPENDS = "makedepends" +CHECKDEPENDS = "checkdepends" +OPTDEPENDS = "optdepends" + +DEPENDS_ID = 1 +MAKEDEPENDS_ID = 2 +CHECKDEPENDS_ID = 3 +OPTDEPENDS_ID = 4 + + +class DependencyType(Base): + __table__ = schema.DependencyTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, Name: str = None): + self.Name = Name diff --git a/aurweb/models/group.py b/aurweb/models/group.py new file mode 100644 index 00000000..0275ed94 --- /dev/null +++ b/aurweb/models/group.py @@ -0,0 +1,18 @@ +from sqlalchemy.exc import IntegrityError + +from aurweb import schema +from aurweb.models.declarative import Base + + +class Group(Base): + __table__ = schema.Groups + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.Name is None: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="Groups.Name", + params=("NULL")) diff --git a/aurweb/models/license.py b/aurweb/models/license.py new file mode 100644 index 00000000..86aeaa86 --- /dev/null +++ b/aurweb/models/license.py @@ -0,0 +1,19 @@ +from sqlalchemy.exc import IntegrityError + +from aurweb import schema +from aurweb.models.declarative import Base + + +class License(Base): + __table__ = schema.Licenses + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="Licenses.Name", + params=("NULL")) diff --git a/aurweb/models/official_provider.py b/aurweb/models/official_provider.py new file mode 100644 index 00000000..e111569e --- /dev/null +++ b/aurweb/models/official_provider.py @@ -0,0 +1,36 @@ +from sqlalchemy.exc import IntegrityError + +from aurweb import schema +from aurweb.models.declarative import Base + +OFFICIAL_BASE = "https://archlinux.org" + + +class OfficialProvider(Base): + __table__ = schema.OfficialProviders + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + # OfficialProvider instances are official packages. + is_official = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="OfficialProviders.Name", + params=("NULL")) + + if not self.Repo: + raise IntegrityError( + statement="Column Repo cannot be null.", + orig="OfficialProviders.Repo", + params=("NULL")) + + if not self.Provides: + raise IntegrityError( + statement="Column Provides cannot be null.", + orig="OfficialProviders.Provides", + params=("NULL")) diff --git a/aurweb/models/package.py b/aurweb/models/package.py new file mode 100644 index 00000000..64c6a195 --- /dev/null +++ b/aurweb/models/package.py @@ -0,0 +1,35 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.package_base import PackageBase as _PackageBase + + +class Package(Base): + __table__ = schema.Packages + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + PackageBase = relationship( + _PackageBase, backref=backref("packages", lazy="dynamic", + cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID]) + + # No Package instances are official packages. + is_official = False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.PackageBase and not self.PackageBaseID: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="Packages.PackageBaseID", + params=("NULL")) + + if self.Name is None: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="Packages.Name", + params=("NULL")) diff --git a/aurweb/models/package_base.py b/aurweb/models/package_base.py new file mode 100644 index 00000000..37ad63ce --- /dev/null +++ b/aurweb/models/package_base.py @@ -0,0 +1,57 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema, time +from aurweb.models.declarative import Base +from aurweb.models.user import User as _User + + +class PackageBase(Base): + __table__ = schema.PackageBases + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + Flagger = relationship( + _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]) + + Maintainer = relationship( + _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]) + + # A set used to check for floatable values. + TO_FLOAT = {"Popularity"} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if self.Name is None: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="PackageBases.Name", + params=("NULL")) + + # If no SubmittedTS/ModifiedTS is provided on creation, set them + # here to the current utc timestamp. + now = time.utcnow() + if not self.SubmittedTS: + self.SubmittedTS = now + if not self.ModifiedTS: + self.ModifiedTS = now + + if not self.FlaggerComment: + self.FlaggerComment = str() + + def __getattribute__(self, key: str): + attr = super().__getattribute__(key) + if key in PackageBase.TO_FLOAT and not isinstance(attr, float): + return float(attr) + return attr diff --git a/aurweb/models/package_blacklist.py b/aurweb/models/package_blacklist.py new file mode 100644 index 00000000..0f8f0cee --- /dev/null +++ b/aurweb/models/package_blacklist.py @@ -0,0 +1,19 @@ +from sqlalchemy.exc import IntegrityError + +from aurweb import schema +from aurweb.models.declarative import Base + + +class PackageBlacklist(Base): + __table__ = schema.PackageBlacklist + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Name: + raise IntegrityError( + statement="Column Name cannot be null.", + orig="PackageBlacklist.Name", + params=("NULL")) diff --git a/aurweb/models/package_comaintainer.py b/aurweb/models/package_comaintainer.py new file mode 100644 index 00000000..b5cdcf38 --- /dev/null +++ b/aurweb/models/package_comaintainer.py @@ -0,0 +1,46 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.package_base import PackageBase as _PackageBase +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] + } + + User = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.User and not self.UsersID: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageComaintainers.UsersID", + params=("NULL")) + + if not self.PackageBase and not self.PackageBaseID: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageComaintainers.PackageBaseID", + params=("NULL")) + + if not self.Priority: + raise IntegrityError( + statement="Column Priority cannot be null.", + orig="PackageComaintainers.Priority", + params=("NULL")) diff --git a/aurweb/models/package_comment.py b/aurweb/models/package_comment.py new file mode 100644 index 00000000..2a529c9c --- /dev/null +++ b/aurweb/models/package_comment.py @@ -0,0 +1,54 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.package_base import PackageBase as _PackageBase +from aurweb.models.user import User as _User + + +class PackageComment(Base): + __table__ = schema.PackageComments + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + PackageBase = relationship( + _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]) + + Editor = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.PackageBase and not self.PackageBaseID: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageComments.PackageBaseID", + params=("NULL")) + + if not self.User and not self.UsersID: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageComments.UsersID", + params=("NULL")) + + if self.Comments is None: + raise IntegrityError( + statement="Column Comments cannot be null.", + orig="PackageComments.Comments", + params=("NULL")) + + if self.RenderedComment is None: + self.RenderedComment = str() diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py new file mode 100644 index 00000000..2fd87f2a --- /dev/null +++ b/aurweb/models/package_dependency.py @@ -0,0 +1,82 @@ +from typing import List + +from sqlalchemy import and_, literal +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import db, schema +from aurweb.models.declarative import Base +from aurweb.models.dependency_type import DependencyType as _DependencyType +from aurweb.models.official_provider import OfficialProvider as _OfficialProvider +from aurweb.models.package import Package as _Package +from aurweb.models.package_relation import PackageRelation + + +class PackageDependency(Base): + __table__ = schema.PackageDepends + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [ + __table__.c.PackageID, + __table__.c.DepTypeID, + __table__.c.DepName, + ] + } + + Package = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Package and not self.PackageID: + raise IntegrityError( + statement="Foreign key PackageID cannot be null.", + orig="PackageDependencies.PackageID", + params=("NULL")) + + if not self.DependencyType and not self.DepTypeID: + raise IntegrityError( + statement="Foreign key DepTypeID cannot be null.", + orig="PackageDependencies.DepTypeID", + params=("NULL")) + + if self.DepName is None: + raise IntegrityError( + statement="Column DepName cannot be null.", + orig="PackageDependencies.DepName", + params=("NULL")) + + 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() + + 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()) + + 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 new file mode 100644 index 00000000..dd212051 --- /dev/null +++ b/aurweb/models/package_group.py @@ -0,0 +1,40 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.group import Group as _Group +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] + } + + Package = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Package and not self.PackageID: + raise IntegrityError( + statement="Primary key PackageID cannot be null.", + orig="PackageGroups.PackageID", + params=("NULL")) + + if not self.Group and not self.GroupID: + raise IntegrityError( + statement="Primary key GroupID cannot be null.", + orig="PackageGroups.GroupID", + params=("NULL")) diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py new file mode 100644 index 00000000..581aafdc --- /dev/null +++ b/aurweb/models/package_keyword.py @@ -0,0 +1,28 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +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] + } + + PackageBase = relationship( + _PackageBase, backref=backref("keywords", lazy="dynamic", + cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.PackageBase and not self.PackageBaseID: + raise IntegrityError( + statement="Primary key PackageBaseID cannot be null.", + orig="PackageKeywords.PackageBaseID", + params=("NULL")) diff --git a/aurweb/models/package_license.py b/aurweb/models/package_license.py new file mode 100644 index 00000000..43dd0339 --- /dev/null +++ b/aurweb/models/package_license.py @@ -0,0 +1,40 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.license import License as _License +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] + } + + Package = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Package and not self.PackageID: + raise IntegrityError( + statement="Primary key PackageID cannot be null.", + orig="PackageLicenses.PackageID", + params=("NULL")) + + if not self.License and not self.LicenseID: + raise IntegrityError( + statement="Primary key LicenseID cannot be null.", + orig="PackageLicenses.LicenseID", + params=("NULL")) diff --git a/aurweb/models/package_notification.py b/aurweb/models/package_notification.py new file mode 100644 index 00000000..327b92a6 --- /dev/null +++ b/aurweb/models/package_notification.py @@ -0,0 +1,41 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.package_base import PackageBase as _PackageBase +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] + } + + User = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.User and not self.UserID: + raise IntegrityError( + statement="Foreign key UserID cannot be null.", + orig="PackageNotifications.UserID", + params=("NULL")) + + if not self.PackageBase and not self.PackageBaseID: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageNotifications.PackageBaseID", + params=("NULL")) diff --git a/aurweb/models/package_relation.py b/aurweb/models/package_relation.py new file mode 100644 index 00000000..4910934d --- /dev/null +++ b/aurweb/models/package_relation.py @@ -0,0 +1,49 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.package import Package as _Package +from aurweb.models.relation_type import RelationType as _RelationType + + +class PackageRelation(Base): + __table__ = schema.PackageRelations + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [ + __table__.c.PackageID, + __table__.c.RelTypeID, + __table__.c.RelName, + ] + } + + Package = relationship( + _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]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Package and not self.PackageID: + raise IntegrityError( + statement="Foreign key PackageID cannot be null.", + orig="PackageRelations.PackageID", + params=("NULL")) + + if not self.RelationType and not self.RelTypeID: + raise IntegrityError( + statement="Foreign key RelTypeID cannot be null.", + orig="PackageRelations.RelTypeID", + params=("NULL")) + + if not self.RelName: + raise IntegrityError( + statement="Column RelName cannot be null.", + orig="PackageRelations.RelName", + params=("NULL")) diff --git a/aurweb/models/package_request.py b/aurweb/models/package_request.py new file mode 100644 index 00000000..9669ec46 --- /dev/null +++ b/aurweb/models/package_request.py @@ -0,0 +1,91 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import 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 +from aurweb.models.user import User as _User + +PENDING = "Pending" +CLOSED = "Closed" +ACCEPTED = "Accepted" +REJECTED = "Rejected" + +# Integer values used for the Status column of PackageRequest. +PENDING_ID = 0 +CLOSED_ID = 1 +ACCEPTED_ID = 2 +REJECTED_ID = 3 + + +class PackageRequest(Base): + __table__ = schema.PackageRequests + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + RequestType = relationship( + _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]) + + PackageBase = relationship( + _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]) + + STATUS_DISPLAY = { + PENDING_ID: PENDING, + CLOSED_ID: CLOSED, + ACCEPTED_ID: ACCEPTED, + REJECTED_ID: REJECTED + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.RequestType and not self.ReqTypeID: + raise IntegrityError( + statement="Foreign key ReqTypeID cannot be null.", + orig="PackageRequests.ReqTypeID", + params=("NULL")) + + if not self.PackageBase and not self.PackageBaseID: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageRequests.PackageBaseID", + params=("NULL")) + + if not self.PackageBaseName: + raise IntegrityError( + statement="Column PackageBaseName cannot be null.", + orig="PackageRequests.PackageBaseName", + params=("NULL")) + + if not self.User and not self.UsersID: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageRequests.UsersID", + params=("NULL")) + + if self.Comments is None: + raise IntegrityError( + statement="Column Comments cannot be null.", + orig="PackageRequests.Comments", + params=("NULL")) + + if self.ClosureComment is None: + raise IntegrityError( + statement="Column ClosureComment cannot be null.", + orig="PackageRequests.ClosureComment", + params=("NULL")) + + def status_display(self) -> str: + """ Return a display string for the Status column. """ + return self.STATUS_DISPLAY[self.Status] diff --git a/aurweb/models/package_source.py b/aurweb/models/package_source.py new file mode 100644 index 00000000..4ea1645b --- /dev/null +++ b/aurweb/models/package_source.py @@ -0,0 +1,34 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +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 + ] + } + + Package = relationship( + _Package, backref=backref("package_sources", lazy="dynamic", + cascade="all, delete"), + foreign_keys=[__table__.c.PackageID]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Package and not self.PackageID: + raise IntegrityError( + statement="Foreign key PackageID cannot be null.", + orig="PackageSources.PackageID", + params=("NULL")) + + if not self.Source: + self.Source = "/dev/null" diff --git a/aurweb/models/package_vote.py b/aurweb/models/package_vote.py new file mode 100644 index 00000000..8da88210 --- /dev/null +++ b/aurweb/models/package_vote.py @@ -0,0 +1,45 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base +from aurweb.models.package_base import PackageBase as _PackageBase +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] + } + + User = relationship( + _User, backref=backref("package_votes", lazy="dynamic"), + foreign_keys=[__table__.c.UsersID]) + + PackageBase = relationship( + _PackageBase, backref=backref("package_votes", lazy="dynamic", + cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.User and not self.UsersID: + raise IntegrityError( + statement="Foreign key UsersID cannot be null.", + orig="PackageVotes.UsersID", + params=("NULL")) + + if not self.PackageBase and not self.PackageBaseID: + raise IntegrityError( + statement="Foreign key PackageBaseID cannot be null.", + orig="PackageVotes.PackageBaseID", + params=("NULL")) + + if not self.VoteTS: + raise IntegrityError( + statement="Column VoteTS cannot be null.", + orig="PackageVotes.VoteTS", + params=("NULL")) diff --git a/aurweb/models/relation_type.py b/aurweb/models/relation_type.py new file mode 100644 index 00000000..b52c91ec --- /dev/null +++ b/aurweb/models/relation_type.py @@ -0,0 +1,19 @@ +from aurweb import schema +from aurweb.models.declarative import Base + +CONFLICTS = "conflicts" +PROVIDES = "provides" +REPLACES = "replaces" + +CONFLICTS_ID = 1 +PROVIDES_ID = 2 +REPLACES_ID = 3 + + +class RelationType(Base): + __table__ = schema.RelationTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, Name: str = None): + self.Name = Name diff --git a/aurweb/models/request_type.py b/aurweb/models/request_type.py new file mode 100644 index 00000000..cabab3d2 --- /dev/null +++ b/aurweb/models/request_type.py @@ -0,0 +1,20 @@ +from aurweb import schema +from aurweb.models.declarative import Base + +DELETION = "deletion" +ORPHAN = "orphan" +MERGE = "merge" + +DELETION_ID = 1 +ORPHAN_ID = 2 +MERGE_ID = 3 + + +class RequestType(Base): + __table__ = schema.RequestTypes + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def name_display(self) -> str: + """ 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 new file mode 100644 index 00000000..37ab4bce --- /dev/null +++ b/aurweb/models/session.py @@ -0,0 +1,39 @@ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import db, schema +from aurweb.models.declarative import Base +from aurweb.models.user import User as _User + + +class Session(Base): + __table__ = schema.Sessions + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.UsersID]} + + User = relationship( + _User, backref=backref("session", uselist=False), + foreign_keys=[__table__.c.UsersID]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + # We'll try to either use UsersID or User.ID if we can. + # If neither exist, an AttributeError is raised, in which case + # we set the uid to 0, which triggers IntegrityError below. + try: + uid = self.UsersID or self.User.ID + except AttributeError: + uid = 0 + + 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."), + orig="Sessions.UsersID", + params=("NULL")) + + +def generate_unique_sid(): + return db.make_random_value(Session, Session.SessionID, 32) diff --git a/aurweb/models/ssh_pub_key.py b/aurweb/models/ssh_pub_key.py new file mode 100644 index 00000000..789be629 --- /dev/null +++ b/aurweb/models/ssh_pub_key.py @@ -0,0 +1,42 @@ +import os +import tempfile + +from subprocess import PIPE, Popen + +from sqlalchemy.orm import backref, relationship + +from aurweb import schema +from aurweb.models.declarative import Base + + +class SSHPubKey(Base): + __table__ = schema.SSHPubKeys + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.Fingerprint]} + + User = relationship( + "User", backref=backref("ssh_pub_key", uselist=False), + foreign_keys=[__table__.c.UserID]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + +def get_fingerprint(pubkey): + with tempfile.TemporaryDirectory() as tmpdir: + pk = os.path.join(tmpdir, "ssh.pub") + + with open(pk, "w") as f: + f.write(pubkey) + + proc = Popen(["ssh-keygen", "-l", "-f", pk], stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + + # Invalid SSH Public Key. Return None to the caller. + if proc.returncode != 0: + return None + + parts = out.decode().split() + fp = parts[1].replace("SHA256:", "") + + return fp diff --git a/aurweb/models/term.py b/aurweb/models/term.py new file mode 100644 index 00000000..59534bbc --- /dev/null +++ b/aurweb/models/term.py @@ -0,0 +1,25 @@ +from sqlalchemy.exc import IntegrityError + +from aurweb import schema +from aurweb.models.declarative import Base + + +class Term(Base): + __table__ = schema.Terms + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.Description: + raise IntegrityError( + statement="Column Description cannot be null.", + orig="Terms.Description", + params=("NULL")) + + if not self.URL: + raise IntegrityError( + statement="Column URL cannot be null.", + orig="Terms.URL", + params=("NULL")) diff --git a/aurweb/models/tu_vote.py b/aurweb/models/tu_vote.py new file mode 100644 index 00000000..efb23b19 --- /dev/null +++ b/aurweb/models/tu_vote.py @@ -0,0 +1,38 @@ +from sqlalchemy.exc import IntegrityError +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 + + +class TUVote(Base): + __table__ = schema.TU_Votes + __tablename__ = __table__.name + __mapper_args__ = { + "primary_key": [__table__.c.VoteID, __table__.c.UserID] + } + + VoteInfo = relationship( + _TUVoteInfo, backref=backref("tu_votes", lazy="dynamic"), + foreign_keys=[__table__.c.VoteID]) + + User = relationship( + _User, backref=backref("tu_votes", lazy="dynamic"), + foreign_keys=[__table__.c.UserID]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + if not self.VoteInfo and not self.VoteID: + raise IntegrityError( + statement="Foreign key VoteID cannot be null.", + orig="TU_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")) diff --git a/aurweb/models/tu_voteinfo.py b/aurweb/models/tu_voteinfo.py new file mode 100644 index 00000000..7934a772 --- /dev/null +++ b/aurweb/models/tu_voteinfo.py @@ -0,0 +1,75 @@ +import typing + +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import backref, relationship + +from aurweb import schema, time +from aurweb.models.declarative import Base +from aurweb.models.user import User as _User + + +class TUVoteInfo(Base): + __table__ = schema.TU_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]) + + def __init__(self, **kwargs): + # Default Quorum, Yes, No and Abstain columns to 0. + for col in ("Quorum", "Yes", "No", "Abstain"): + if col not in kwargs: + kwargs.update({col: 0}) + + super().__init__(**kwargs) + + if self.Agenda is None: + raise IntegrityError( + statement="Column Agenda cannot be null.", + orig="TU_VoteInfo.Agenda", + params=("NULL")) + + if self.User is None: + raise IntegrityError( + statement="Column User cannot be null.", + orig="TU_VoteInfo.User", + params=("NULL")) + + if self.Submitted is None: + raise IntegrityError( + statement="Column Submitted cannot be null.", + orig="TU_VoteInfo.Submitted", + params=("NULL")) + + if self.End is None: + raise IntegrityError( + statement="Column End cannot be null.", + orig="TU_VoteInfo.End", + params=("NULL")) + + if not self.Submitter: + raise IntegrityError( + statement="Foreign key SubmitterID cannot be null.", + orig="TU_VoteInfo.SubmitterID", + params=("NULL")) + + def __setattr__(self, key: str, value: typing.Any): + """ 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. """ + attr = super().__getattribute__(key) + if key == "Quorum": + return float(attr) + return attr + + def is_running(self): + return self.End > time.utcnow() + + def total_votes(self): + return self.Yes + self.No + self.Abstain diff --git a/aurweb/models/user.py b/aurweb/models/user.py new file mode 100644 index 00000000..871ff209 --- /dev/null +++ b/aurweb/models/user.py @@ -0,0 +1,256 @@ +import hashlib + +from typing import List, Set + +import bcrypt + +from fastapi import Request +from sqlalchemy import or_ +from sqlalchemy.exc import IntegrityError +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.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__) + +SALT_ROUNDS_DEFAULT = 12 + + +class User(Base): + """ An ORM model of a single Users record. """ + __table__ = schema.Users + __tablename__ = __table__.name + __mapper_args__ = {"primary_key": [__table__.c.ID]} + + AccountType = relationship( + _AccountType, + backref=backref("users", lazy="dynamic"), + foreign_keys=[__table__.c.AccountTypeID], + uselist=False) + + # High-level variables used to track authentication (not in DB). + authenticated = False + nonce = None + + # 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) + + 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) + if Passwd: + self.update_password(Passwd) + + def update_password(self, password): + self.Passwd = bcrypt.hashpw( + 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 self.authenticated + + def valid_password(self, password: str): + """ 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()) + 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 + + # We got here, we passed the legacy authentication. + # Update the password to our modern hash style. + if password_is_valid: + self.update_password(password) + + return password_is_valid + + 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. """ + + from aurweb import db + from aurweb.models.session import Session, generate_unique_sid + + if not self._login_approved(request): + return None + + self.authenticated = self.valid_password(password) + if not self.authenticated: + return None + + # Maximum number of iterations where we attempt to generate + # a unique SID. In cases where the Session table has + # exhausted all possible values, this will catch exceptions + # instead of raising them and include details about failing + # generation in an HTTPException. + tries = 36 + + exc = None + for i in range(tries): + exc = None + now_ts = time.utcnow() + try: + with db.begin(): + self.LastLogin = now_ts + self.LastLoginIPAddress = request.client.host + if not self.session: + sid = generate_unique_sid() + 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: + self.session.SessionID = generate_unique_sid() + self.session.LastUpdateTS = now_ts + break + except IntegrityError as exc_: + exc = exc_ + + if exc: + raise exc + + return self.session.SessionID + + 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): + self.authenticated = False + if self.session: + with db.begin(): + db.delete(self.session) + + def is_trusted_user(self): + return self.AccountType.ID in { + aurweb.models.account_type.TRUSTED_USER_ID, + aurweb.models.account_type.TRUSTED_USER_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 + } + + def is_elevated(self): + """ A User is 'elevated' when they have either a + Trusted User or Developer AccountType. """ + return self.AccountType.ID in { + aurweb.models.account_type.TRUSTED_USER_ID, + aurweb.models.account_type.DEVELOPER_ID, + aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID, + } + + def can_edit_user(self, target: "User") -> bool: + """ + Whether this User instance can edit `target`. + + This User can edit user `target` if we both: have credentials and + self.AccountTypeID is greater or equal to `target`.AccountTypeID. + + 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 + + :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? """ + from aurweb.models.package_vote import PackageVote + 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)? + + :param package: Package or PackageBase instance + :return: Boolean indicating state of package notification + in relation to this User + """ + from aurweb.models.package import Package + from aurweb.models.package_base import PackageBase + from aurweb.models.package_notification import PackageNotification + + query = None + if isinstance(package, Package): + query = package.PackageBase.notifications + elif isinstance(package, PackageBase): + query = package.notifications + + # 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()) + + def packages(self): + """ 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 + due to issues I've been encountering in the process, so + sticking with this function until we can properly implement it. + + :return: ORM query of User-packaged or maintained Package objects + """ + 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 + ) + ) + + def __repr__(self): + return "" % ( + self.ID, str(self.AccountType), self.Username) + + def __str__(self) -> str: + return self.Username + + +def generate_resetkey(): + return util.make_random_string(32) diff --git a/aurweb/packages/__init__.py b/aurweb/packages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aurweb/packages/requests.py b/aurweb/packages/requests.py new file mode 100644 index 00000000..724249d4 --- /dev/null +++ b/aurweb/packages/requests.py @@ -0,0 +1,235 @@ +from typing import List, Optional, Set + +from fastapi import Request +from sqlalchemy import and_, orm + +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.scripts import notify + + +class ClosureFactory: + """ A factory class used to autogenerate closure comments. """ + + 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 _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 _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: + """ + Return a closure comment handled by this class. + + :param reqtype_id: RequestType.ID + :param requester: User who is closing a request + :param pkgbase: PackageBase instance related to the request + :param target: Merge request target PackageBase instance + :param status: PackageRequest.Status + """ + reqtype = ClosureFactory.REQTYPE_NAMES.get(reqtype_id) + + partial = str() + if status == REJECTED_ID: + partial = "_rejected" + + try: + handler = getattr(self, f"{partial}_{reqtype}_closure") + except AttributeError: + raise NotImplementedError("Unsupported 'reqtype_id' value.") + return handler(requester, pkgbase, target) + + +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. + + In order to persist closure comments through `handle_request`'s + algorithm, we must set `PackageRequest.ClosureComment` before calling + it. This function can be used to update the closure comment of all + package requests related to `pkgbase` and `reqtype_id`. + + If an empty `comments` string is provided, we no-op out of this. + + :param pkgbase: PackageBase instance + :param reqtype_id: RequestType.ID + :param comments: PackageRequest.ClosureComment to update to + :param target: Merge request target PackageBase instance + """ + if not comments: + return + + query = pkgbase.requests.filter( + and_(PackageRequest.ReqTypeID == reqtype_id, + PackageRequest.Status == PENDING_ID)) + if reqtype_id == MERGE_ID: + query = query.filter(PackageRequest.MergeBaseName == target.Name) + + for pkgreq in query: + pkgreq.ClosureComment = comments + + +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) + for pkgreq in requests: + idle_time = config.getint("options", "request_idle_time") + time_delta = time.utcnow() - pkgreq.RequestTS + is_due = pkgreq.Status == PENDING_ID and time_delta > idle_time + if is_due: + # If the requester is the pkgbase maintainer or the + # request is already due, we're good to go: return True. + return True + + return False + + +def close_pkgreq(pkgreq: PackageRequest, closer: User, + pkgbase: PackageBase, target: Optional[PackageBase], + status: int) -> None: + """ + Close a package request with `pkgreq`.Status == `status`. + + :param pkgreq: PackageRequest instance + :param closer: `pkgreq`.Closer User instance to update to + :param pkgbase: PackageBase instance which `pkgreq` is about + :param target: Optional PackageBase instance to merge into + :param status: `pkgreq`.Status value to update to + """ + now = time.utcnow() + pkgreq.Status = status + pkgreq.Closer = closer + 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]: + """ + Handle package requests before performing an action. + + The actions we're interested in are disown (orphan), delete and + merge. There is now an automated request generation and closure + notification when a privileged user performs one of these actions + without a pre-existing request. They all commit changes to the + database, and thus before calling, state should be verified to + avoid leaked database records regarding these requests. + + Otherwise, we accept and reject requests based on their state + and send out the relevent notifications. + + :param requester: User who needs this a `pkgbase` request handled + :param reqtype_id: RequestType.ID + :param pkgbase: PackageBase which the request is about + :param target: Optional target to merge into + """ + 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) + + # 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)) + + # Build a query for records we should accept. For merge requests, + # this is specific to a matching MergeBaseName. For others, this + # just ends up becoming `query`. + accept_query: orm.Query = query + if target: + # If a `target` was supplied, filter by MergeBaseName + accept_query = query.filter( + PackageRequest.MergeBaseName == target.Name) + + # Build an accept list out of `accept_query`. + 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( + ~PackageRequest.ID.in_(accepted_ids) + ).all() + + # If we have no requests to accept, create a new one. + # This is done to increase tracking of actions occurring + # through the website. + if not to_accept: + with db.begin(): + pkgreq = db.create(PackageRequest, + ReqTypeID=reqtype_id, + User=request.user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments="Autogenerated by aurweb.", + ClosureComment=str()) + + # If it's a merge request, set MergeBaseName to `target`.Name. + if pkgreq.ReqTypeID == MERGE_ID: + pkgreq.MergeBaseName = target.Name + + # Add the new request to `to_accept` and allow standard + # flow to continue afterward. + 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)) + + # Create RequestCloseNotifications for all requests involved. + for pkgreq in (to_accept + to_reject): + notif = notify.RequestCloseNotification( + request.user.ID, pkgreq.ID, pkgreq.status_display()) + notifs.append(notif) + + # Return notifications to the caller for sending. + return notifs diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py new file mode 100644 index 00000000..b4af5aab --- /dev/null +++ b/aurweb/packages/search.py @@ -0,0 +1,320 @@ +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.package_comaintainer import PackageComaintainer +from aurweb.models.package_keyword import PackageKeyword +from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_vote import PackageVote + + +class PackageSearch: + """ A Package search query builder. """ + + # A constant mapping of short to full name sort orderings. + FULL_SORT_ORDER = {"d": "desc", "a": "asc"} + + def __init__(self, user: models.User = None): + self.query = db.query(Package).join(PackageBase) + + self.user = user + if self.user: + self.query = self.query.join( + PackageVote, + 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 + ) + + self.ordering = "d" + + # Setup SeB (Search By) callbacks. + self.search_by_cb = { + "nd": self._search_by_namedesc, + "n": self._search_by_name, + "b": self._search_by_pkgbase, + "N": self._search_by_exact_name, + "B": self._search_by_exact_pkgbase, + "k": self._search_by_keywords, + "m": self._search_by_maintainer, + "c": self._search_by_comaintainer, + "M": self._search_by_co_or_maintainer, + "s": self._search_by_submitter + } + + # Setup SB (Sort By) callbacks. + self.sort_by_cb = { + "n": self._sort_by_name, + "v": self._sort_by_votes, + "p": self._sort_by_popularity, + "w": self._sort_by_voted, + "o": self._sort_by_notify, + "m": self._sort_by_maintainer, + "l": self._sort_by_last_modified + } + + self._joined = False + + def _join_user(self, outer: bool = True) -> orm.Query: + """ Centralized joining of a package base's maintainer. """ + if not self._joined: + self.query = self.query.join( + User, + User.ID == PackageBase.MaintainerUID, + isouter=outer + ) + self._joined = True + return self.query + + 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}%")) + ) + return self + + def _search_by_name(self, keywords: str) -> orm.Query: + self._join_user() + self.query = self.query.filter(Package.Name.like(f"%{keywords}%")) + return self + + def _search_by_exact_name(self, keywords: str) -> orm.Query: + self._join_user() + self.query = self.query.filter(Package.Name == keywords) + return self + + def _search_by_pkgbase(self, keywords: str) -> orm.Query: + self._join_user() + self.query = self.query.filter(PackageBase.Name.like(f"%{keywords}%")) + return self + + def _search_by_exact_pkgbase(self, keywords: str) -> orm.Query: + self._join_user() + self.query = self.query.filter(PackageBase.Name == keywords) + return self + + def _search_by_keywords(self, keywords: str) -> orm.Query: + self._join_user() + self.query = self.query.join(PackageKeyword).filter( + PackageKeyword.Keyword == keywords + ) + 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) + ) + else: + self.query = self.query.filter(PackageBase.MaintainerUID.is_(None)) + return self + + def _search_by_comaintainer(self, keywords: str) -> orm.Query: + self._join_user() + exists_subq = db.query(PackageComaintainer).join(User).filter( + and_(PackageComaintainer.PackageBaseID == PackageBase.ID, + User.Username == keywords) + ).exists() + self.query = self.query.filter(db.query(exists_subq).scalar_subquery()) + return self + + def _search_by_co_or_maintainer(self, keywords: str) -> orm.Query: + self._join_user() + exists_subq = db.query(PackageComaintainer).join(User).filter( + and_(PackageComaintainer.PackageBaseID == PackageBase.ID, + User.Username == keywords) + ).exists() + self.query = self.query.filter( + or_(and_(User.Username == keywords, + User.ID == PackageBase.MaintainerUID), + db.query(exists_subq).scalar_subquery()) + ) + return self + + def _search_by_submitter(self, keywords: str) -> orm.Query: + self._join_user() + + uid = 0 + user = db.query(User).filter(User.Username == keywords).first() + if user: + uid = user.ID + + self.query = self.query.filter(PackageBase.SubmitterUID == uid) + return self + + def search_by(self, search_by: str, keywords: str) -> orm.Query: + if search_by not in self.search_by_cb: + search_by = "nd" # Default: Name, Description + callback = self.search_by_cb.get(search_by) + result = callback(keywords) + return result + + def _sort_by_name(self, order: str): + column = getattr(models.Package.Name, order) + self.query = self.query.order_by(column()) + return self + + def _sort_by_votes(self, order: str): + column = getattr(models.PackageBase.NumVotes, order) + name = getattr(models.Package.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) + self.query = self.query.order_by(column(), name()) + return self + + def _sort_by_voted(self, order: str): + # FIXME: Currently, PHP is destroying this implementation + # 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 + ) + name = getattr(models.Package.Name, order) + self.query = self.query.order_by(column(), name()) + return self + + def _sort_by_notify(self, order: str): + # FIXME: Currently, PHP is destroying this implementation + # 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 + ) + name = getattr(models.Package.Name, order) + self.query = self.query.order_by(column(), name()) + return self + + def _sort_by_maintainer(self, order: str): + column = getattr(models.User.Username, order) + name = getattr(models.Package.Name, order) + self.query = self.query.order_by(column(), name()) + return self + + def _sort_by_last_modified(self, order: str): + column = getattr(models.PackageBase.ModifiedTS, order) + name = getattr(models.Package.Name, order) + self.query = self.query.order_by(column(), name()) + return self + + def sort_by(self, sort_by: str, ordering: str = "d") -> orm.Query: + if sort_by not in self.sort_by_cb: + sort_by = "p" # Default: Popularity + callback = self.sort_by_cb.get(sort_by) + if ordering not in self.FULL_SORT_ORDER: + ordering = "d" # Default: Descending + ordering = self.FULL_SORT_ORDER.get(ordering) + return callback(ordering) + + def count(self, limit: int) -> int: + """ + Return internal query's count up to `limit`. + + :param limit: Upper bound + :return: Database count up to `limit` + """ + return self.query.limit(limit).count() + + def results(self) -> orm.Query: + """ Return internal query. """ + return self.query + + +class RPCSearch(PackageSearch): + """ 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 + constructor, we pop unneeded keys out of inherited self.search_by_cb + and add a few more keys to it, namely: depends, makedepends, + optdepends and checkdepends. + + Additionally, some logic within the inherited PackageSearch.search_by + method is not needed, so it is overridden in this class without + sanitization done for the PackageSearch `by` argument. + """ + + keys_removed = ("b", "N", "B", "k", "c", "M", "s") + + def __init__(self) -> "RPCSearch": + super().__init__() + + # Fix-up inherited search_by_cb to reflect RPC-specific by params. + # 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() + 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 + }) + + # 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 + 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) + return self.query + + def _search_by_depends(self, keywords: str) -> "RPCSearch": + self.query = self._join_depends(DEPENDS_ID).filter( + 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) + return self + + def _search_by_optdepends(self, keywords: str) -> "RPCSearch": + self.query = self._join_depends(OPTDEPENDS_ID).filter( + 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) + return self + + def search_by(self, by: str, keywords: str) -> "RPCSearch": + """ 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. + + :param by: RPC `by` argument + :param keywords: RPC `arg` argument + :returns: self + """ + callback = self.search_by_cb.get(by) + result = callback(keywords) + return result + + def results(self) -> orm.Query: + return self.query.filter(models.PackageBase.PackagerUID.isnot(None)) diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py new file mode 100644 index 00000000..21b5fab8 --- /dev/null +++ b/aurweb/packages/util.py @@ -0,0 +1,261 @@ +from collections import defaultdict +from http import HTTPStatus +from typing import Dict, List, Tuple, Union + +import orjson + +from fastapi import HTTPException +from sqlalchemy import orm + +from aurweb import config, db, models +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]] + + +def dep_extra_with_arch(dep: models.PackageDependency, annotation: str) -> str: + output = [annotation] + if dep.DepArch: + output.append(dep.DepArch) + return f"({', '.join(output)})" + + +def dep_depends_extra(dep: models.PackageDependency) -> str: + return str() + + +def dep_makedepends_extra(dep: models.PackageDependency) -> str: + return dep_extra_with_arch(dep, "make") + + +def dep_checkdepends_extra(dep: models.PackageDependency) -> str: + return dep_extra_with_arch(dep, "check") + + +def dep_optdepends_extra(dep: models.PackageDependency) -> str: + return dep_extra_with_arch(dep, "optional") + + +@register_filter("dep_extra") +def dep_extra(dep: models.PackageDependency) -> str: + """ 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. """ + f = globals().get(f"dep_{dep.DependencyType.Name}_extra") + return f(dep) + + +@register_filter("dep_extra_desc") +def dep_extra_desc(dep: models.PackageDependency) -> str: + extra = dep_extra(dep) + if not dep.DepDesc: + return extra + return extra + f" – {dep.DepDesc}" + + +@register_filter("pkgname_link") +def pkgname_link(pkgname: str) -> str: + official = db.query(OfficialProvider).filter( + OfficialProvider.Name == pkgname).exists() + if db.query(official).scalar(): + base = "/".join([OFFICIAL_BASE, "packages"]) + return f"{base}/?q={pkgname}" + return f"/packages/{pkgname}" + + +@register_filter("package_link") +def package_link(package: Union[Package, OfficialProvider]) -> str: + if package.is_official: + base = "/".join([OFFICIAL_BASE, "packages"]) + return f"{base}/?q={package.Name}" + return f"/packages/{package.Name}" + + +@register_filter("provides_markup") +def provides_markup(provides: Providers) -> str: + return ", ".join([ + f'{pkg.Name}' + for pkg in provides + ]) + + +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 + it can't be found in the database. + + :param name: {Package,PackageBase}.Name + :param exception: Whether to raise an HTTPException or simply return None if + the package can't be found. + :raises HTTPException: With status code 404 if record doesn't exist + :return: {Package,PackageBase} instance + """ + provider = db.query(models.OfficialProvider).filter( + models.OfficialProvider.Name == name).first() + if provider: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + with db.begin(): + 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: + comment = pkgbase.comments.filter(models.PackageComment.ID == id).first() + if not comment: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + return db.refresh(comment) + + +@register_filter("out_of_date") +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 + ModifiedTS column in descending order from cache, after setting + the cache when no key yet exists. + + :param limit: Optional record limit + :param cache_ttl: Cache expiration time (in seconds) + :return: A list of Packages + """ + redis = redis_connection() + packages = redis.get("package_updates") + if packages: + # If we already have a cache, deserialize it and return. + return orjson.loads(packages) + + query = db.query(models.Package).join(models.PackageBase).filter( + models.PackageBase.PackagerUID.isnot(None) + ).order_by( + models.PackageBase.ModifiedTS.desc() + ) + + 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. + db.refresh(pkg) + 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)) + redis.expire("package_updates", cache_ttl) + + # Return the deserialized list of packages. + 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, + which indicate whether or not the package base has a vote record + related to user. + + :param query: A collection of Package models + :param user: The user that is being notified or not + :return: Vote state dict (PackageBase.ID: int -> bool) + """ + 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 + ) + 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, + which indicate whether or not the package base has a notification + record related to user. + + :param query: A collection of Package models + :param user: The user that is being notified or not + :return: Notification state dict (PackageBase.ID: int -> bool) + """ + 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 + ) + for notif in notified: + output[notif.PackageBase.ID] = True + return output + + +def pkg_required(pkgname: str, provides: List[str], limit: int) \ + -> List[PackageDependency]: + """ + Get dependencies that match a string in `[pkgname] + provides`. + + :param pkgname: Package.Name + :param provides: List of PackageRelation.Name + :param limit: Maximum number of dependencies to query + :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() + + +@register_filter("source_uri") +def source_uri(pkgsrc: models.PackageSource) -> Tuple[str, str]: + """ + Produce a (text, uri) tuple out of `pkgsrc`. + + In this filter, we cover various cases: + 1. If "::" is anywhere in the Source column, split the string, + which should produce a (text, uri), where text is before "::" + and uri is after "::". + 2. Otherwise, if "://" is anywhere in the Source column, it's just + some sort of URI, which we'll return varbatim as both text and uri. + 3. Otherwise, we'll return a path to the source file in a uri produced + out of options.source_file_uri formatted with the source file and + the package base name. + + :param pkgsrc: PackageSource instance + :return (text, uri) tuple + """ + if "::" in pkgsrc.Source: + return pkgsrc.Source.split("::", 1) + elif "://" in 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)) diff --git a/aurweb/pkgbase/__init__.py b/aurweb/pkgbase/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py new file mode 100644 index 00000000..73366829 --- /dev/null +++ b/aurweb/pkgbase/actions.py @@ -0,0 +1,141 @@ +from typing import List + +from fastapi import Request + +from aurweb import db, logging, util +from aurweb.auth import creds +from aurweb.models import PackageBase +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 +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__) + + +def pkgbase_notify_instance(request: Request, pkgbase: PackageBase) -> None: + 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) + + +def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: + notif = pkgbase.notifications.filter( + PackageNotification.UserID == request.user.ID + ).first() + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) + if has_cred and notif: + with db.begin(): + db.delete(notif) + + +def pkgbase_unflag_instance(request: Request, pkgbase: PackageBase) -> None: + has_cred = request.user.has_credential( + creds.PKGBASE_UNFLAG, approved=[pkgbase.Flagger, pkgbase.Maintainer]) + if has_cred: + with db.begin(): + pkgbase.OutOfDateTS = None + pkgbase.Flagger = None + pkgbase.FlaggerComment = str() + + +def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: + disowner = request.user + notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)] + + is_maint = disowner == pkgbase.Maintainer + if is_maint: + with db.begin(): + # Comaintainer with the lowest Priority value; next-in-line. + prio_comaint = pkgbase.comaintainers.order_by( + PackageComaintainer.Priority.asc() + ).first() + if prio_comaint: + # If there is such a comaintainer, promote them to maint. + pkgbase.Maintainer = prio_comaint.User + notifs.append(pkgbaseutil.remove_comaintainer(prio_comaint)) + else: + # Otherwise, just orphan the package completely. + pkgbase.Maintainer = None + 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. + notifs += handle_request(request, ORPHAN_ID, pkgbase) + with db.begin(): + pkgbase.Maintainer = None + + util.apply_all(notifs, lambda n: n.send()) + + +def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: + with db.begin(): + pkgbase.Maintainer = request.user + + 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) + ] + + with db.begin(): + update_closure_comment(pkgbase, DELETION_ID, comments) + db.delete(pkgbase) + + 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) + + # 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) + target_notifs = set(n.UserID for n in target.notifications) + + with db.begin(): + # Merge pkgbase's comments. + for comment in pkgbase.comments: + comment.PackageBase = target + + # Merge notifications that don't yet exist in the target. + for notif in pkgbase.notifications: + if notif.UserID not in target_notifs: + notif.PackageBase = target + + # Merge votes that don't yet exist in the target. + for vote in pkgbase.package_votes: + if vote.UsersID not in target_votes: + vote.PackageBase = target + + # Run popupdate. + popupdate.run_single(target) + + with db.begin(): + # Delete pkgbase and its packages now that everything's merged. + for pkg in pkgbase.packages: + db.delete(pkg) + db.delete(pkgbase) + + # Log this out for accountability purposes. + logger.info(f"Trusted User '{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 new file mode 100644 index 00000000..76b8a8c9 --- /dev/null +++ b/aurweb/pkgbase/util.py @@ -0,0 +1,196 @@ +from typing import Any, Dict, List + +from fastapi import Request +from sqlalchemy import and_ + +from aurweb import config, db, l10n, util +from aurweb.models import PackageBase, User +from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_comment import PackageComment +from aurweb.models.package_request import PENDING_ID, PackageRequest +from aurweb.models.package_vote import PackageVote +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. + + :param request: FastAPI request + :param pkgbase: PackageBase instance + :return: A pkgbase context without specific differences + """ + context = _make_context(request, pkgbase.Name) + + 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["packages_count"] = pkgbase.packages.count() + context["keywords"] = pkgbase.keywords + context["comments"] = pkgbase.comments.order_by( + PackageComment.CommentTS.desc() + ) + 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) + + context["out_of_date"] = bool(pkgbase.OutOfDateTS) + + context["voted"] = request.user.package_votes.filter( + PackageVote.PackageBaseID == pkgbase.ID + ).scalar() + + context["requests"] = pkgbase.requests.filter( + and_(PackageRequest.Status == PENDING_ID, + PackageRequest.ClosedTS.is_(None)) + ).count() + + return context + + +def remove_comaintainer(comaint: PackageComaintainer) \ + -> notify.ComaintainerRemoveNotification: + """ + Remove a PackageComaintainer. + + This function does *not* begin any database transaction and + must be used **within** a database transaction, e.g.: + + with db.begin(): + remove_comaintainer(comaint) + + :param comaint: Target PackageComaintainer to be deleted + :return: ComaintainerRemoveNotification + """ + pkgbase = comaint.PackageBase + notif = notify.ComaintainerRemoveNotification(comaint.User.ID, pkgbase.ID) + db.delete(comaint) + rotate_comaintainers(pkgbase) + return notif + + +def remove_comaintainers(pkgbase: PackageBase, usernames: List[str]) -> None: + """ + Remove comaintainers from `pkgbase`. + + :param pkgbase: PackageBase instance + :param usernames: Iterable of username strings + """ + notifications = [] + with db.begin(): + comaintainers = pkgbase.comaintainers.join(User).filter( + User.Username.in_(usernames) + ).all() + notifications = [ + notify.ComaintainerRemoveNotification(co.User.ID, pkgbase.ID) + for co in comaintainers + ] + db.delete_all(comaintainers) + + # Rotate comaintainer priority values. + with db.begin(): + rotate_comaintainers(pkgbase) + + # Send out notifications. + util.apply_all(notifications, lambda n: n.send()) + + +def latest_priority(pkgbase: PackageBase) -> int: + """ + Return the highest Priority column related to `pkgbase`. + + :param pkgbase: PackageBase instance + :return: Highest Priority found or 0 if no records exist + """ + + # Order comaintainers related to pkgbase by Priority DESC. + 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. """ + + def send(self) -> None: + """ noop """ + return + + +def add_comaintainer(pkgbase: PackageBase, comaintainer: User) \ + -> notify.ComaintainerAddNotification: + """ + Add a new comaintainer to `pkgbase`. + + :param pkgbase: PackageBase instance + :param comaintainer: User instance used for new comaintainer record + :return: ComaintainerAddNotification + """ + # Skip given `comaintainers` who are already maintainer. + if pkgbase.Maintainer == comaintainer: + return NoopComaintainerNotification() + + # Priority for the new comaintainer is +1 more than the highest. + new_prio = latest_priority(pkgbase) + 1 + + with db.begin(): + 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: + """ + Add comaintainers to `pkgbase`. + + :param request: FastAPI request + :param pkgbase: PackageBase instance + :param usernames: Iterable of username strings + :return: Error string on failure else None + """ + # For each username in usernames, perform validation of the username + # and append the User record to `users` if no errors occur. + users = [] + for username in usernames: + user = db.query(User).filter(User.Username == username).first() + if not user: + _ = l10n.get_translator_for_request(request) + return _("Invalid user name: %s") % username + users.append(user) + + notifications = [] + + def add_comaint(user: User): + nonlocal notifications + # Populate `notifications` with add_comaintainer's return value, + # which is a ComaintainerAddNotification. + notifications.append(add_comaintainer(pkgbase, user)) + + # Move along: add all `users` as new `pkgbase` comaintainers. + util.apply_all(users, add_comaint) + + # Send out notifications. + util.apply_all(notifications, lambda n: n.send()) + + +def rotate_comaintainers(pkgbase: PackageBase) -> None: + """ + Rotate `pkgbase` comaintainers. + + This function resets the Priority column of all PackageComaintainer + instances related to `pkgbase` to seqential 1 .. n values with + persisted order. + + :param pkgbase: PackageBase instance + """ + 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 new file mode 100644 index 00000000..8d05a3d7 --- /dev/null +++ b/aurweb/pkgbase/validate.py @@ -0,0 +1,35 @@ +from typing import Any, Dict + +from aurweb import 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."]) + + 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.']) + + 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." + ]) + + 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." + ]) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py new file mode 100644 index 00000000..dae56320 --- /dev/null +++ b/aurweb/prometheus.py @@ -0,0 +1,103 @@ +from typing import Any, Callable, Dict, List, Optional + +from prometheus_client import Counter +from prometheus_fastapi_instrumentator import Instrumentator +from prometheus_fastapi_instrumentator.metrics import Info +from starlette.routing import Match, Route + +from aurweb import logging + +logger = logging.get_logger(__name__) +_instrumentator = Instrumentator() + + +def instrumentator(): + return _instrumentator + + +# 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: + """ + Find a matching route and return its original path string + + Will attempt to enter mounted routes and subrouters. + + Credit to https://github.com/elastic/apm-agent-python + + """ + for route in routes: + match, child_scope = route.matches(scope) + 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. + child_scope = {**scope, **child_scope} + if isinstance(route, Mount) and route.routes: + child_route_name = get_matching_route_path(child_scope, + route.routes, + route_name) + if child_route_name is None: + route_name = None + else: + route_name += child_route_name + ''' + + return route_name + elif match == Match.PARTIAL and route_name is None: + route_name = route.path + + +def http_requests_total() -> Callable[[Info], None]: + metric = Counter("http_requests_total", + "Number of HTTP requests.", + labelnames=("method", "path", "status")) + + def instrumentation(info: Info) -> None: + scope = info.request.scope + + # Taken from https://github.com/stephenhillier/starlette_exporter + # Their license is included at LICENSES/starlette_exporter. + # The code has been slightly modified: we no longer catch + # exceptions; we expect this collector to always succeed. + # Failures in this collector shall cause test failures. + if not (scope.get("endpoint", None) and scope.get("router", None)): + return None + + base_scope = { + "type": scope.get("type"), + "path": scope.get("root_path", "") + scope.get("path"), + "path_params": scope.get("path_params", {}), + "method": scope.get("method") + } + + method = scope.get("method") + path = get_matching_route_path(base_scope, scope.get("router").routes) + + if info.response: + status = str(int(info.response.status_code))[:1] + "xx" + metric.labels(method=method, path=path, status=status).inc() + + return instrumentation + + +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")) + + def instrumentation(info: Info) -> None: + if info.request.url.path.rstrip("/") == "/rpc": + type = info.request.query_params.get("type", "None") + if info.response: + status = str(info.response.status_code)[:1] + "xx" + metric.labels(type=type, status=status).inc() + + return instrumentation diff --git a/aurweb/ratelimit.py b/aurweb/ratelimit.py new file mode 100644 index 00000000..659ab6b8 --- /dev/null +++ b/aurweb/ratelimit.py @@ -0,0 +1,110 @@ +from fastapi import Request +from redis.client import Pipeline + +from aurweb import config, db, logging, time +from aurweb.models import ApiRateLimit +from aurweb.redis import redis_connection + +logger = logging.get_logger(__name__) + + +def _update_ratelimit_redis(request: Request, pipeline: Pipeline): + window_length = config.getint("ratelimit", "window_length") + now = time.utcnow() + time_to_delete = now - window_length + + host = request.client.host + window_key = f"ratelimit-ws:{host}" + requests_key = f"ratelimit:{host}" + + pipeline.get(window_key) + window = pipeline.execute()[0] + + if not window or int(window.decode()) < time_to_delete: + pipeline.set(window_key, now) + pipeline.expire(window_key, window_length) + + pipeline.set(requests_key, 1) + pipeline.expire(requests_key, window_length) + + pipeline.execute() + else: + pipeline.incr(requests_key) + pipeline.execute() + + +def _update_ratelimit_db(request: Request): + window_length = config.getint("ratelimit", "window_length") + 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) + + host = request.client.host + 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 + + logger.debug(record.Requests) + return record + + +def update_ratelimit(request: Request, pipeline: Pipeline): + """ 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 + is not configured to use a real server, this function instead uses + the database to persist tracking of a particular host. + + :param request: FastAPI request + :param pipeline: redis.client.Pipeline + :returns: ApiRateLimit record when Redis cache is not configured, else None + """ + if config.getboolean("ratelimit", "cache"): + return _update_ratelimit_redis(request, pipeline) + return _update_ratelimit_db(request) + + +def check_ratelimit(request: Request): + """ 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 + """ + redis = redis_connection() + pipeline = redis.pipeline() + + record = update_ratelimit(request, pipeline) + + # Get cache value, else None. + host = request.client.host + pipeline.get(f"ratelimit:{host}") + requests = pipeline.execute()[0] + + # Take into account the split paths. When Redis is used, a + # 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 we got nothing from pipeline.get, we did not use + # the Redis path of logic: use the DB record's count. + requests = record.Requests + else: + # Otherwise, just case Redis results over to an int. + requests = int(requests.decode()) + + limit = config.getint("ratelimit", "request_limit") + exceeded_ratelimit = requests > limit + if exceeded_ratelimit: + logger.debug(f"{host} has exceeded the ratelimit.") + + return exceeded_ratelimit diff --git a/aurweb/redis.py b/aurweb/redis.py new file mode 100644 index 00000000..e29b8e37 --- /dev/null +++ b/aurweb/redis.py @@ -0,0 +1,57 @@ +import fakeredis + +from redis import ConnectionPool, Redis + +import aurweb.config + +from aurweb import logging + +logger = logging.get_logger(__name__) +pool = None + + +class FakeConnectionPool: + """ A fake ConnectionPool class which holds an internal reference + to a fakeredis handle. + + We normally deal with Redis by keeping its ConnectionPool globally + referenced so we can persist connection state through different calls + to redis_connection(), and since FakeRedis does not offer a ConnectionPool, + we craft one up here to hang onto the same handle instance as long as the + same instance is alive; this allows us to use a similar flow from the + redis_connection() user's perspective. + """ + + def __init__(self): + self.handle = fakeredis.FakeStrictRedis() + + def disconnect(self): + pass + + +def redis_connection(): # pragma: no cover + global pool + + disabled = aurweb.config.get("options", "cache") != "redis" + + # If we haven't initialized redis yet, construct a pool. + if disabled: + if pool is None: + logger.debug("Initializing fake Redis instance.") + pool = FakeConnectionPool() + return pool.handle + else: + if pool is None: + logger.debug("Initializing real Redis instance.") + redis_addr = aurweb.config.get("options", "redis_address") + pool = ConnectionPool.from_url(redis_addr) + + # Create a connection to the pool. + return Redis(connection_pool=pool) + + +def kill_redis(): + global pool + if pool: + pool.disconnect() + pool = None diff --git a/aurweb/requests/__init__.py b/aurweb/requests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aurweb/requests/util.py b/aurweb/requests/util.py new file mode 100644 index 00000000..97c3447e --- /dev/null +++ b/aurweb/requests/util.py @@ -0,0 +1,13 @@ +from http import HTTPStatus + +from fastapi import HTTPException + +from aurweb import db +from aurweb.models import PackageRequest + + +def get_pkgreq_by_id(id: int) -> PackageRequest: + pkgreq = db.query(PackageRequest).filter(PackageRequest.ID == id).first() + if not pkgreq: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + return db.refresh(pkgreq) diff --git a/aurweb/routers/__init__.py b/aurweb/routers/__init__.py new file mode 100644 index 00000000..da79e38f --- /dev/null +++ b/aurweb/routers/__init__.py @@ -0,0 +1,24 @@ +""" +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 + +""" +aurweb application routes. This constant can be any iterable +and each element must have a .router attribute which points +to a fastapi.APIRouter. +""" +APP_ROUTES = [ + accounts, + auth, + html, + packages, + pkgbase, + requests, + trusted_user, + rss, + rpc, + sso, +] diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py new file mode 100644 index 00000000..33ca9ed7 --- /dev/null +++ b/aurweb/routers/accounts.py @@ -0,0 +1,633 @@ +import copy +import typing + +from http import HTTPStatus + +from fastapi import APIRouter, Form, 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.captcha import get_captcha_salts +from aurweb.exceptions import ValidationError +from aurweb.l10n import get_translator_for_request +from aurweb.models import account_type as at +from aurweb.models.ssh_pub_key import get_fingerprint +from aurweb.models.user import generate_resetkey +from aurweb.scripts.notify import ResetKeyNotification, WelcomeNotification +from aurweb.templates import make_context, make_variable_context, render_template +from aurweb.users import update, validate +from aurweb.users.util import get_user_by_name + +router = APIRouter() +logger = logging.get_logger(__name__) + + +@router.get("/passreset", response_class=HTMLResponse) +@requires_guest +async def passreset(request: Request): + context = await make_variable_context(request, "Password Reset") + return render_template(request, "passreset.html", context) + + +@router.post("/passreset", response_class=HTMLResponse) +@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)): + context = await make_variable_context(request, "Password Reset") + + # The user parameter being required, we can match against + user = db.query(models.User, or_(models.User.Username == user, + models.User.Email == user)).first() + if not user: + context["errors"] = ["Invalid e-mail."] + return render_template(request, "passreset.html", context, + status_code=HTTPStatus.NOT_FOUND) + + db.refresh(user) + if resetkey: + context["resetkey"] = resetkey + + if not user.ResetKey or resetkey != user.ResetKey: + context["errors"] = ["Invalid e-mail."] + 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) + + 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) + + 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) + + # We got to this point; everything matched up. Update the password + # and remove the ResetKey. + with db.begin(): + user.ResetKey = str() + if user.session: + db.delete(user.session) + user.update_password(password) + + # Render ?step=complete. + 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() + with db.begin(): + user.ResetKey = resetkey + + ResetKeyNotification(user.ID).send() + + # Render ?step=confirm. + return RedirectResponse(url="/passreset?step=confirm", + status_code=HTTPStatus.SEE_OTHER) + + +def process_account_form(request: Request, user: models.User, args: dict): + """ Process an account form. All fields are optional and only checks + requirements in the case they are present. + + ``` + context = await make_variable_context(request, "Accounts") + ok, errors = process_account_form(request, user, **kwargs) + if not ok: + context["errors"] = errors + return render_template(request, "some_account_template.html", context) + ``` + + :param request: An incoming FastAPI request + :param user: The user model of the account being processed + :param args: A dictionary of arguments generated via request.form() + :return: A (passed processing boolean, list of errors) tuple + """ + + # Get a local translator. + _ = get_translator_for_request(request) + + checks = [ + validate.is_banned, + validate.invalid_user_password, + validate.invalid_fields, + validate.invalid_suspend_permission, + validate.invalid_username, + validate.invalid_password, + validate.invalid_email, + validate.invalid_backup_email, + validate.invalid_homepage, + validate.invalid_pgp_key, + validate.invalid_ssh_pubkey, + validate.invalid_language, + validate.invalid_timezone, + validate.username_in_use, + validate.email_in_use, + validate.invalid_account_type, + validate.invalid_captcha + ] + + try: + for check in checks: + check(**args, request=request, user=user, _=_) + except ValidationError as exc: + return (False, exc.data) + + 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. + + :param context: FastAPI context + :param request: FastAPI request + :param user: Target user + :param args: Persistent arguments: request.form() + :return: FastAPI context adjusted for account form + """ + # 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) + ] + )) + + if request.user.is_authenticated(): + context["username"] = args.get("U", user.Username) + context["account_type"] = args.get("T", user.AccountType.ID) + context["suspended"] = args.get("S", user.Suspended) + context["email"] = args.get("E", user.Email) + context["hide_email"] = args.get("H", user.HideEmail) + context["backup_email"] = args.get("BE", user.BackupEmail) + context["realname"] = args.get("R", user.RealName) + context["homepage"] = args.get("HP", user.Homepage or str()) + context["ircnick"] = args.get("I", user.IRCNick) + context["pgp"] = args.get("K", user.PGPKey or str()) + context["lang"] = args.get("L", user.LangPreference) + context["tz"] = args.get("TZ", user.Timezone) + ssh_pk = user.ssh_pub_key.PubKey if user.ssh_pub_key else str() + context["ssh_pk"] = args.get("PK", ssh_pk) + context["cn"] = args.get("CN", user.CommentNotify) + context["un"] = args.get("UN", user.UpdateNotify) + context["on"] = args.get("ON", user.OwnershipNotify) + context["inactive"] = args.get("J", user.InactivityTS != 0) + else: + context["username"] = args.get("U", str()) + context["account_type"] = args.get("T", at.USER_ID) + context["suspended"] = args.get("S", False) + context["email"] = args.get("E", str()) + context["hide_email"] = args.get("H", False) + context["backup_email"] = args.get("BE", str()) + context["realname"] = args.get("R", str()) + context["homepage"] = args.get("HP", str()) + context["ircnick"] = args.get("I", str()) + context["pgp"] = args.get("K", str()) + context["lang"] = args.get("L", context.get("language")) + context["tz"] = args.get("TZ", context.get("timezone")) + context["ssh_pk"] = args.get("PK", str()) + context["cn"] = args.get("CN", True) + context["un"] = args.get("UN", False) + context["on"] = args.get("ON", True) + context["inactive"] = args.get("J", False) + + context["password"] = args.get("P", str()) + context["confirm"] = args.get("C", str()) + + return context + + +@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())): + 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) + + +@router.post("/register", response_class=HTMLResponse) +@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=None), # 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(...)): + context = await make_variable_context(request, "Register") + args = dict(await request.form()) + + context = make_account_form_context(context, request, None, args) + ok, errors = process_account_form(request, request.user, args) + if not ok: + # 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) + + if not captcha: + context["errors"] = ["The CAPTCHA is missing."] + 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() + + # 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) + + # 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. + pubkey = PK.strip().rstrip() + parts = pubkey.split(" ") + if len(parts) == 3: + # Remove the host part. + pubkey = parts[0] + " " + parts[1] + fingerprint = get_fingerprint(pubkey) + with db.begin(): + user.ssh_pub_key = models.SSHPubKey(UserID=user.ID, + PubKey=pubkey, + Fingerprint=fingerprint) + + # Send a reset key notification to the new user. + WelcomeNotification(user.ID).send() + + context["complete"] = True + context["user"] = user + return render_template(request, "register.html", context) + + +def cannot_edit(request: Request, user: models.User) \ + -> typing.Optional[RedirectResponse]: + """ + Decide if `request.user` cannot edit `user`. + + If the request user can edit the target user, None is returned. + Otherwise, a redirect is returned to /account/{user.Username}. + + :param request: FastAPI request + :param user: Target user to be edited + :return: RedirectResponse if approval != granted else None + """ + approved = request.user.can_edit_user(user) + if not approved and (to := "/"): + if user: + to = f"/account/{user.Username}" + return RedirectResponse(to, status_code=HTTPStatus.SEE_OTHER) + return None + + +@router.get("/account/{username}/edit", response_class=HTMLResponse) +@requires_auth +async def account_edit(request: Request, username: str): + user = db.query(models.User, models.User.Username == username).first() + + response = cannot_edit(request, user) + if response: + return response + + context = await make_variable_context(request, "Accounts") + context["user"] = db.refresh(user) + + context = make_account_form_context(context, request, user, dict()) + return render_template(request, "account/edit.html", context) + + +@router.post("/account/{username}/edit", response_class=HTMLResponse) +@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() + response = cannot_edit(request, user) + if response: + return response + + context = await make_variable_context(request, "Accounts") + context["user"] = db.refresh(user) + + args = dict(await request.form()) + context = make_account_form_context(context, request, user, args) + ok, errors = process_account_form(request, user, args) + + if not passwd: + context["errors"] = ["Invalid password."] + 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) + + updates = [ + update.simple, + update.language, + update.timezone, + update.ssh_pubkey, + update.account_type, + update.password + ] + + 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) + + +@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) + if not request.user.is_authenticated(): + return render_template(request, "account/show.html", context, + status_code=HTTPStatus.UNAUTHORIZED) + + # Get related User record, if possible. + user = get_user_by_name(username) + context["user"] = user + + # 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)]) + + login_ts = None + session = db.query(models.Session).filter( + models.Session.UsersID == user.ID).first() + if session: + login_ts = user.session.LastUpdateTS + context["login_ts"] = login_ts + + # Render the template. + return render_template(request, "account/show.html", context) + + +@router.get("/account/{username}/comments") +@requires_auth +async def account_comments(request: Request, username: str): + user = get_user_by_name(username) + context = make_context(request, "Accounts") + context["username"] = username + context["comments"] = user.package_comments.order_by( + 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}) +async def accounts(request: Request): + context = make_context(request, "Accounts") + return render_template(request, "account/search.html", context) + + +@router.post("/accounts") +@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 + context = await make_variable_context(request, "Accounts") + context["pp"] = pp = 50 # Hits per page. + + offset = max(O, 0) # Minimize offset at 0. + context["offset"] = offset # Offset. + + context["params"] = dict(await request.form()) + if "O" in context["params"]: + context["params"].pop("O") + + # Setup order by criteria based on SB. + order_by_columns = { + "t": (models.AccountType.ID.asc(), models.User.Username.asc()), + "r": (models.User.RealName.asc(), models.AccountType.ID.asc()), + "i": (models.User.IRCNick.asc(), models.AccountType.ID.asc()), + } + default_order = (models.User.Username.asc(), models.AccountType.ID.asc()) + order_by = order_by_columns.get(SB, default_order) + + # Convert parameter T to an AccountType ID. + account_types = { + "u": at.USER_ID, + "t": at.TRUSTED_USER_ID, + "d": at.DEVELOPER_ID, + "td": at.TRUSTED_USER_AND_DEV_ID + } + account_type_id = account_types.get(T, None) + + # Get a query handle to users, populate the total user + # count into a jinja2 context variable. + query = db.query(models.User).join(models.AccountType) + + # Populate this list with any additional statements to + # be ANDed together. + statements = [ + 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), + (bool(E), models.User.Email.like(f"%{E}%")), + (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 + ] + + # Filter the query by coe-mbining all statements added above into + # an AND statement, unless there's just one statement, which + # we pass on to filter() as args. + if statements: + query = query.filter(and_(*statements)) + + context["total_users"] = query.count() + + # Finally, order and truncate our users for the current page. + users = query.order_by(*order_by).limit(pp).offset(offset).all() + context["users"] = util.apply_all(users, db.refresh) + + return render_template(request, "account/index.html", context) + + +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 + return render_template(request, "tos/index.html", context) + + +@router.get("/tos") +@requires_auth +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() + + # 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() + + for record in (diffs + unaccepted): + db.refresh(record) + + # Translate the 'Terms of Service' part of our page title. + _ = l10n.get_translator_for_request(request) + title = f"AUR {_('Terms of Service')}" + context = await make_variable_context(request, title) + + accept_needed = sorted(unaccepted + diffs) + return render_terms_of_service(request, context, accept_needed) + + +@router.post("/tos") +@requires_auth +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() + + # 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() + + if not accept: + # Translate the 'Terms of Service' part of our page title. + _ = l10n.get_translator_for_request(request) + title = f"AUR {_('Terms of Service')}" + context = await make_variable_context(request, title) + + # We already did the database filters here, so let's just use + # 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)) + + with db.begin(): + # For each term we found, query for the matching accepted term + # and update its Revision to the term's current Revision. + for term in diffs: + db.refresh(term) + accepted_term = request.user.accepted_terms.filter( + 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) + + return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py new file mode 100644 index 00000000..bae9a599 --- /dev/null +++ b/aurweb/routers/auth.py @@ -0,0 +1,93 @@ +from http import HTTPStatus + +from fastapi import APIRouter, Form, HTTPException, Request +from fastapi.responses import HTMLResponse, RedirectResponse + +import aurweb.config + +from aurweb import cookies, db, time +from aurweb.auth import requires_auth, requires_guest +from aurweb.l10n import get_translator_for_request +from aurweb.models import User +from aurweb.templates import make_variable_context, render_template + +router = APIRouter() + + +async def login_template(request: Request, next: str, errors: list = None): + """ 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}" + return render_template(request, "login.html", context) + + +@router.get("/login", response_class=HTMLResponse) +async def login_get(request: Request, next: str = "/"): + return await login_template(request, next) + + +@router.post("/login", response_class=HTMLResponse) +@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)): + # 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.")) + + user = db.query(User).filter(User.Username == user).first() + if not user: + return await login_template(request, next, + errors=["Bad username or password."]) + + cookie_timeout = cookies.timeout(remember_me) + sid = user.login(request, passwd, cookie_timeout) + if not sid: + 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) + + 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()) + return response + + +@router.post("/logout") +@requires_auth +async def logout(request: Request, next: str = Form(default="/")): + if request.user.is_authenticated(): + request.user.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.delete_cookie("AURSID") + response.delete_cookie("AURTZ") + return response diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py new file mode 100644 index 00000000..74901762 --- /dev/null +++ b/aurweb/routers/html.py @@ -0,0 +1,223 @@ +""" 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 + +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_ + +import aurweb.config +import aurweb.models.package_request + +from aurweb import cookies, db, models, time, util +from aurweb.cache import db_count_cache +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 + +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. """ + return RedirectResponse("/static/images/favicon.ico") + + +@router.post("/language", response_class=RedirectResponse) +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. + + Return a 303 See Other redirect to {next}?next={next}. If we are + setting the language on any page, we want to preserve query + parameters across the redirect. + """ + if next[0] != '/': + return HTMLResponse(b"Invalid 'next' parameter.", status_code=400) + + query_string = "?" + q if q else str() + + # If the user is authenticated, update the user's LangPreference. + if request.user.is_authenticated(): + with db.begin(): + request.user.LangPreference = set_lang + + # 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. """ + context = make_context(request, "Home") + context['ssh_fingerprints'] = util.get_ssh_fingerprints() + + bases = db.query(models.PackageBase) + + redis = aurweb.redis.redis_connection() + cache_expire = 300 # Five minutes. + + # 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) + + # Get the 15 most recently updated packages. + context["package_updates"] = updated_packages(15, cache_expire) + + if request.user.is_authenticated(): + # Authenticated users get a few extra pieces of data for + # the dashboard display. + packages = db.query(models.Package).join(models.PackageBase) + + maintained = packages.join( + models.User, models.PackageBase.MaintainerUID == models.User.ID + ).filter( + models.PackageBase.MaintainerUID == 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() + + # Flagged packages that request.user has voted for. + context["flagged_packages_voted"] = query_voted( + 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) + + archive_time = aurweb.config.getint('options', 'request_archive_time') + start = now - 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() + + # Packages that the request user maintains or comaintains. + context["packages"] = maintained.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) + + # Packages that request.user is being notified about. + context["packages_notified"] = query_notified( + 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() + + # Comaintained packages that request.user has voted for. + context["comaintained_voted"] = query_voted( + context.get("comaintained"), request.user) + + # Comaintained packages that request.user is being notified about. + context["comaintained_notified"] = query_notified( + context.get("comaintained"), request.user) + + return render_template(request, "index.html", context) + + +@router.get("/metrics") +async def metrics(request: Request): + registry = CollectorRegistry() + if os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): # pragma: no cover + multiprocess.MultiProcessCollector(registry) + data = generate_latest(registry) + headers = { + "Content-Type": CONTENT_TYPE_LATEST, + "Content-Length": str(len(data)) + } + return Response(data, headers=headers) + + +@router.get("/raisefivethree", response_class=HTMLResponse) +async def raise_service_unavailable(request: Request): + raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py new file mode 100644 index 00000000..79c6a404 --- /dev/null +++ b/aurweb/routers/packages.py @@ -0,0 +1,440 @@ +from collections import defaultdict +from http import HTTPStatus +from typing import Any, Dict, List + +from fastapi import APIRouter, Form, Request, Response + +import aurweb.filters # noqa: F401 + +from aurweb import config, db, defaults, logging, models, util +from aurweb.auth import creds, requires_auth +from aurweb.exceptions import InvariantError +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.templates import make_context, render_template + +logger = logging.get_logger(__name__) +router = APIRouter() + + +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)) + context["O"] = offset + context["PP"] = per_page + + # Query search by. + search_by = context["SeB"] = request.query_params.get("SeB", "nd") + + # Query sort by. + sort_by = context["SB"] = request.query_params.get("SB", "p") + + # Query sort order. + sort_order = request.query_params.get("SO", None) + + # Apply ordering, limit and offset. + search = PackageSearch(request.user) + + # For each keyword found in K, apply a search_by filter. + # This means that for any sentences separated by spaces, + # they are used as if they were ANDed. + keywords = context["K"] = request.query_params.get("K", str()) + keywords = keywords.split(" ") + for keyword in keywords: + search.search_by(search_by, keyword) + + # 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. + limit = config.getint("options", "max_search_results") + num_packages = search.count(limit) + + flagged = request.query_params.get("outdated", None) + if flagged: + # If outdated was given, set it up in the context. + context["outdated"] = flagged + + # When outdated is set to "on," we filter records which do have + # an OutOfDateTS. When it's set to "off," we filter out any which + # do **not** have OutOfDateTS. + criteria = None + if flagged == "on": + criteria = models.PackageBase.OutOfDateTS.isnot + else: + criteria = models.PackageBase.OutOfDateTS.is_ + + # Apply the flag criteria to our PackageSearch.query. + search.query = search.query.filter(criteria(None)) + + submit = request.query_params.get("submit", "Go") + 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)) + + # Apply user-specified specified sort column and ordering. + search.sort_by(sort_by, sort_order) + + # If no SO was given, default the context SO to 'a' (Ascending). + # By default, if no SO is given, the search should sort by 'd' + # (Descending), but display "Ascending" for the Sort order select. + if sort_order is None: + sort_order = "a" + context["SO"] = sort_order + + # Insert search results into the context. + results = search.results().with_entities( + models.Package.ID, + models.Package.Name, + models.Package.PackageBaseID, + models.Package.Version, + models.Package.Description, + models.PackageBase.Popularity, + models.PackageBase.NumVotes, + models.PackageBase.OutOfDateTS, + models.User.Username.label("Maintainer"), + models.PackageVote.PackageBaseID.label("Voted"), + models.PackageNotification.PackageBaseID.label("Notify") + ) + + 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) + + +@router.get("/packages") +async def packages(request: Request) -> Response: + context = make_context(request, "Packages") + return await packages_get(request, context) + + +@router.get("/packages/{name}") +async def package(request: Request, name: str) -> Response: + # Get the Package. + pkg = get_pkg_or_base(name, models.Package) + pkgbase = pkg.PackageBase + + rels = pkg.package_relations.order_by(models.PackageRelation.RelName.asc()) + rels_data = defaultdict(list) + for rel in rels: + if rel.RelTypeID == CONFLICTS_ID: + rels_data["c"].append(rel) + elif rel.RelTypeID == PROVIDES_ID: + rels_data["p"].append(rel) + elif rel.RelTypeID == REPLACES_ID: + rels_data["r"].append(rel) + + # Add our base information. + context = pkgbaseutil.make_context(request, pkgbase) + context["package"] = pkg + + # Package sources. + context["sources"] = pkg.package_sources.order_by( + models.PackageSource.Source.asc()).all() + + # 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() + + # 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) + + context["licenses"] = pkg.package_licenses + + conflicts = pkg.package_relations.filter( + models.PackageRelation.RelTypeID == CONFLICTS_ID + ).order_by(models.PackageRelation.RelName.asc()) + context["conflicts"] = conflicts + + provides = pkg.package_relations.filter( + models.PackageRelation.RelTypeID == PROVIDES_ID + ).order_by(models.PackageRelation.RelName.asc()) + context["provides"] = provides + + replaces = pkg.package_relations.filter( + models.PackageRelation.RelTypeID == REPLACES_ID + ).order_by(models.PackageRelation.RelName.asc()) + context["replaces"] = replaces + + return render_template(request, "packages/show.html", context) + + +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."]) + + # 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() + for pkg in packages: + has_cred = request.user.has_credential( + creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger]) + if not has_cred: + 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."]) + + +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."]) + 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() + + for pkg in packages: + if pkg.PackageBase not in bases: + bases.update({pkg.PackageBase}) + + # 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() + has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) + + # If the request user either does not have credentials + # or the notification already exists: + if not (has_cred and not notif): + return error_tuple + + # If we get here, user input is good. + for pkgbase in bases: + 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."]) + + +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."]) + + # TODO: This error does not yet have a translation. + error_tuple = ( + False, + ["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() + + for pkg in packages: + if pkg.PackageBase not in bases: + bases.update({pkg.PackageBase}) + + # 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() + if not notif: + return error_tuple + + for pkgbase in bases: + 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."]) + + +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."]) + + if not confirm: + 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() + + for pkg in packages: + if pkg.PackageBase not in bases: + bases.update({pkg.PackageBase}) + + # Check that the user has credentials for every package they selected. + for pkgbase in bases: + 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."]) + + # Now, really adopt the bases. + for pkgbase in bases: + pkgbase_actions.pkgbase_adopt_instance(request, pkgbase) + + return (True, ["The selected packages have been adopted."]) + + +def disown_all(request: Request, pkgbases: List[models.PackageBase]) \ + -> List[str]: + errors = [] + for pkgbase in pkgbases: + try: + pkgbase_actions.pkgbase_disown_instance(request, pkgbase) + except InvariantError as exc: + errors.append(str(exc)) + return errors + + +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."]) + + if not confirm: + 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() + + for pkg in packages: + if pkg.PackageBase not in bases: + bases.update({pkg.PackageBase}) + + # 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]) + 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."]) + + # Now, disown all the bases if we can. + if errors := disown_all(request, bases): + return (False, errors) + + 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): + if not package_ids: + 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."]) + + if not request.user.has_credential(creds.PKGBASE_DELETE): + 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() + + 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."]) + + # Make a set out of all package bases related to `packages`. + bases = {pkg.PackageBase for pkg in packages} + deleted_bases, notifs = [], [] + for pkgbase in bases: + deleted_bases.append(pkgbase.Name) + 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)}.") + + util.apply_all(notifs, lambda n: n.send()) + 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]). +PACKAGE_ACTIONS = { + "unflag": packages_unflag, + "notify": packages_notify, + "unnotify": packages_unnotify, + "adopt": packages_adopt, + "disown": packages_disown, + "delete": packages_delete, +} + + +@router.post("/packages") +@requires_auth +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: + context = make_context(request, "Packages") + return await packages_get(request, context, HTTPStatus.BAD_REQUEST) + + context = make_context(request, "Packages") + + # We deal with `IDs`, `merge_into` and `confirm` arguments + # within action callbacks. + callback = PACKAGE_ACTIONS.get(action) + retval = await callback(request, package_ids=IDs, confirm=confirm) + if retval: # If *anything* was returned: + success, messages = retval + if not success: + # If the first element was False: + context["errors"] = messages + return await packages_get(request, context, HTTPStatus.BAD_REQUEST) + else: + # Otherwise: + context["success"] = messages + + return await packages_get(request, context) diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py new file mode 100644 index 00000000..3aa73a02 --- /dev/null +++ b/aurweb/routers/pkgbase.py @@ -0,0 +1,856 @@ +from http import HTTPStatus + +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.auth import creds, requires_auth +from aurweb.exceptions import InvariantError, ValidationError +from aurweb.models import PackageBase +from aurweb.models.package_comment import PackageComment +from aurweb.models.package_keyword import PackageKeyword +from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, PackageRequest +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.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__) +router = APIRouter() + + +@router.get("/pkgbase/{name}") +async def pkgbase(request: Request, name: str) -> Response: + """ + Single package base view. + + :param request: FastAPI Request + :param name: PackageBase.Name + :return: HTMLResponse + """ + # Get the PackageBase. + pkgbase = get_pkg_or_base(name, PackageBase) + + # If this is not a split package, redirect to /packages/{name}. + if pkgbase.packages.count() == 1: + return RedirectResponse(f"/packages/{name}", + status_code=int(HTTPStatus.SEE_OTHER)) + + # Add our base information. + context = pkgbaseutil.make_context(request, pkgbase) + context["packages"] = pkgbase.packages.all() + + return render_template(request, "pkgbase/index.html", context) + + +@router.get("/pkgbase/{name}/voters") +async def pkgbase_voters(request: Request, name: str) -> Response: + """ + View of package base voters. + + Requires `request.user` has creds.PKGBASE_LIST_VOTERS credential. + + :param request: FastAPI Request + :param name: PackageBase.Name + :return: HTMLResponse + """ + # Get the PackageBase. + 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) + + context = templates.make_context(request, "Voters") + context["pkgbase"] = pkgbase + return render_template(request, "pkgbase/voters.html", context) + + +@router.get("/pkgbase/{name}/flag-comment") +async def pkgbase_flag_comment(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + if pkgbase.Flagger is None: + 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) + + +@router.post("/pkgbase/{name}/keywords") +async def pkgbase_keywords(request: Request, name: str, + keywords: str = Form(default=str())): + pkgbase = get_pkg_or_base(name, PackageBase) + keywords = set(keywords.split(" ")) + + # Delete all keywords which are not supplied by the user. + other_keywords = pkgbase.keywords.filter( + ~PackageKeyword.Keyword.in_(keywords)) + other_keyword_strings = [kwd.Keyword for kwd in other_keywords] + + existing_keywords = set( + kwd.Keyword for kwd in + pkgbase.keywords.filter( + ~PackageKeyword.Keyword.in_(other_keyword_strings)) + ) + with db.begin(): + db.delete_all(other_keywords) + for keyword in keywords.difference(existing_keywords): + db.create(PackageKeyword, + PackageBase=pkgbase, + Keyword=keyword) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=HTTPStatus.SEE_OTHER) + + +@router.get("/pkgbase/{name}/flag") +@requires_auth +async def pkgbase_flag_get(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential(creds.PKGBASE_FLAG) + if not has_cred or pkgbase.Flagger is not None: + 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) + + +@router.post("/pkgbase/{name}/flag") +@requires_auth +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) + + has_cred = request.user.has_credential(creds.PKGBASE_FLAG) + if has_cred and not pkgbase.Flagger: + now = time.utcnow() + with db.begin(): + pkgbase.OutOfDateTS = now + pkgbase.Flagger = request.user + pkgbase.FlaggerComment = comments + + return RedirectResponse(f"/pkgbase/{name}", + status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/comments") +@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. """ + pkgbase = get_pkg_or_base(name, PackageBase) + + if not comment: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST) + + # 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) + + if enable_notifications and not request.user.notified(pkgbase): + db.create(PackageNotification, + User=request.user, + PackageBase=pkgbase) + update_comment_render_fastapi(comment) + + # Redirect to the pkgbase page. + 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)): + """ + Produce a comment form for comment {id}. + + This route is used as a partial HTML endpoint when editing + package comments via Javascript. This endpoint used to be + part of the RPC as type=get-comment-form and has been + relocated here because the form returned cannot be used + externally and requires a POST request by the user. + + :param request: FastAPI Request + :param name: PackageBase.Name + :param id: PackageComment.ID + :param next: Optional `next` value used for the comment form + :return: JSONResponse + """ + pkgbase = get_pkg_or_base(name, PackageBase) + comment = pkgbase.comments.filter(PackageComment.ID == id).first() + if not comment: + return JSONResponse({}, status_code=HTTPStatus.NOT_FOUND) + + if not request.user.is_elevated() and request.user != comment.User: + return JSONResponse({}, status_code=HTTPStatus.UNAUTHORIZED) + + context = pkgbaseutil.make_context(request, pkgbase) + context["comment"] = comment + + if not next: + next = f"/pkgbase/{name}" + + context["next"] = next + + form = templates.render_raw_template( + 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)): + """ + Render the non-javascript edit form. + + :param request: FastAPI Request + :param name: PackageBase.Name + :param id: PackageComment.ID + :param next: Optional `next` parameter used in the POST request + :return: HTMLResponse + """ + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + if not next: + next = f"/pkgbase/{name}" + + context = await make_variable_context(request, "Edit comment", next=next) + context["comment"] = comment + return render_template(request, "pkgbase/comments/edit.html", context) + + +@router.post("/pkgbase/{name}/comments/{id}") +@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. """ + pkgbase = get_pkg_or_base(name, PackageBase) + db_comment = get_pkgbase_comment(pkgbase, id) + + if not comment: + raise HTTPException(status_code=HTTPStatus.BAD_REQUEST) + + # If the provided comment is different than the record's version, + # update the db record. + now = time.utcnow() + if db_comment.Comments != comment: + with db.begin(): + db_comment.Comments = comment + db_comment.Editor = request.user + db_comment.EditedTS = now + + 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) + 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) + + +@router.post("/pkgbase/{name}/comments/{id}/pin") +@requires_auth +async def pkgbase_comment_pin(request: Request, name: str, id: int, + next: str = Form(default=None)): + """ + Pin a comment. + + :param request: FastAPI Request + :param name: PackageBase.Name + :param id: PackageComment.ID + :param next: Optional `next` parameter used in the POST request + :return: RedirectResponse to `next` + """ + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + has_cred = request.user.has_credential(creds.COMMENT_PIN, + approved=[pkgbase.Maintainer]) + 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.")) + + now = time.utcnow() + with db.begin(): + comment.PinnedTS = now + + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/comments/{id}/unpin") +@requires_auth +async def pkgbase_comment_unpin(request: Request, name: str, id: int, + next: str = Form(default=None)): + """ + Unpin a comment. + + :param request: FastAPI Request + :param name: PackageBase.Name + :param id: PackageComment.ID + :param next: Optional `next` parameter used in the POST request + :return: RedirectResponse to `next` + """ + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + has_cred = request.user.has_credential(creds.COMMENT_PIN, + approved=[pkgbase.Maintainer]) + 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.")) + + with db.begin(): + comment.PinnedTS = 0 + + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/comments/{id}/delete") +@requires_auth +async def pkgbase_comment_delete(request: Request, name: str, id: int, + next: str = Form(default=None)): + """ + Delete a comment. + + This action does **not** delete the comment from the database, but + sets PackageBase.DelTS and PackageBase.DeleterUID, which is used to + decide who gets to view the comment and what utilities it gets. + + :param request: FastAPI Request + :param name: PackageBase.Name + :param id: PackageComment.ID + :param next: Optional `next` parameter used in the POST request + :return: RedirectResposne to `next` + """ + pkgbase = get_pkg_or_base(name, PackageBase) + comment = get_pkgbase_comment(pkgbase, id) + + 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.")) + + now = time.utcnow() + with db.begin(): + comment.Deleter = request.user + comment.DelTS = now + + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/comments/{id}/undelete") +@requires_auth +async def pkgbase_comment_undelete(request: Request, name: str, id: int, + next: str = Form(default=None)): + """ + Undelete a comment. + + This action does **not** undelete any comment from the database, but + unsets PackageBase.DelTS and PackageBase.DeleterUID which restores + the comment to a standard state. + + :param request: FastAPI Request + :param name: PackageBase.Name + :param id: PackageComment.ID + :param next: Optional `next` parameter used in the POST request + :return: RedirectResponse to `next` + """ + 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]) + 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.")) + + with db.begin(): + comment.Deleter = None + comment.DelTS = None + + if not next: + next = f"/pkgbase/{name}" + + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/vote") +@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() + 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) + + # Update NumVotes/Popularity. + popupdate.run_single(pkgbase) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/unvote") +@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() + has_cred = request.user.has_credential(creds.PKGBASE_VOTE) + if has_cred and vote: + with db.begin(): + db.delete(vote) + + # Update NumVotes/Popularity. + popupdate.run_single(pkgbase) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/notify") +@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) + + +@router.post("/pkgbase/{name}/unnotify") +@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) + + +@router.post("/pkgbase/{name}/unflag") +@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) + + +@router.get("/pkgbase/{name}/disown") +@requires_auth +async def pkgbase_disown_get(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, + approved=[pkgbase.Maintainer]) + if not has_cred: + return RedirectResponse(f"/pkgbase/{name}", + HTTPStatus.SEE_OTHER) + + context = templates.make_context(request, "Disown Package") + context["pkgbase"] = pkgbase + return render_template(request, "pkgbase/disown.html", context) + + +@router.post("/pkgbase/{name}/disown") +@requires_auth +async def pkgbase_disown_post(request: Request, name: str, + comments: str = Form(default=str()), + confirm: bool = Form(default=False)): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, + approved=[pkgbase.Maintainer]) + if not has_cred: + 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) + + 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) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=HTTPStatus.SEE_OTHER) + + +@router.post("/pkgbase/{name}/adopt") +@requires_auth +async def pkgbase_adopt_post(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + + has_cred = request.user.has_credential(creds.PKGBASE_ADOPT) + if has_cred or not pkgbase.Maintainer: + # If the user has credentials, they'll adopt the package regardless + # of maintainership. Otherwise, we'll promote the user to maintainer + # if no maintainer currently exists. + actions.pkgbase_adopt_instance(request, pkgbase) + + return RedirectResponse(f"/pkgbase/{name}", + status_code=HTTPStatus.SEE_OTHER) + + +@router.get("/pkgbase/{name}/comaintainers") +@requires_auth +async def pkgbase_comaintainers(request: Request, name: 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]) + if not has_creds: + 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 + ] + }) + + return render_template(request, "pkgbase/comaintainers.html", context) + + +@router.post("/pkgbase/{name}/comaintainers") +@requires_auth +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]) + if not has_creds: + 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}") + + 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["errors"] = [error] + return render_template(request, "pkgbase/comaintainers.html", context) + + 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) + + +@router.get("/pkgbase/{name}/request") +@requires_auth +async def pkgbase_request(request: Request, name: str): + pkgbase = get_pkg_or_base(name, PackageBase) + context = await make_variable_context(request, "Submit Request") + context["pkgbase"] = pkgbase + return render_template(request, "pkgbase/request.html", context) + + +@router.post("/pkgbase/{name}/request") +@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())): + 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 + } + + 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) + + try: + validate.request(pkgbase, type, comments, merge_into, context) + except ValidationError as exc: + logger.error(f"Request Validation Error: {str(exc.data)}") + context["errors"] = exc.data + return render_template(request, "pkgbase/request.html", context) + + # 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()) + + # Prepare notification object. + notif = notify.RequestOpenNotification( + 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() + + auto_orphan_age = config.getint("options", "auto_orphan_age") + auto_delete_age = config.getint("options", "auto_delete_age") + + ood_ts = pkgbase.OutOfDateTS or 0 + flagged = ood_ts and (now - ood_ts) >= auto_orphan_age + is_maintainer = pkgbase.Maintainer == request.user + outdated = (now - pkgbase.SubmittedTS) <= auto_delete_age + + if type == "orphan" and flagged: + # This request should be auto-accepted. + with db.begin(): + pkgbase.Maintainer = None + pkgreq.Status = ACCEPTED_ID + notif = notify.RequestCloseNotification( + 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) + util.apply_all(notifs, lambda n: n.send()) + logger.debug(f"New request #{pkgreq.ID} is marked for auto-deletion.") + + # Redirect the submitting user to /packages. + return RedirectResponse("/packages", status_code=HTTPStatus.SEE_OTHER) + + +@router.get("/pkgbase/{name}/delete") +@requires_auth +async def pkgbase_delete_get(request: Request, name: str): + if not request.user.has_credential(creds.PKGBASE_DELETE): + 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) + return render_template(request, "pkgbase/delete.html", context) + + +@router.post("/pkgbase/{name}/delete") +@requires_auth +async def pkgbase_delete_post(request: Request, name: str, + confirm: bool = Form(default=False), + comments: str = Form(default=str())): + 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) + + 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) + + if comments: + # Update any existing deletion requests' ClosureComment. + with db.begin(): + requests = pkgbase.requests.filter( + 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) + util.apply_all(notifs, lambda n: n.send()) + return RedirectResponse("/packages", 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())): + pkgbase = get_pkg_or_base(name, PackageBase) + + context = templates.make_context(request, "Package Merging") + context.update({ + "pkgbase": pkgbase, + "into": into, + "next": next + }) + + status_code = HTTPStatus.OK + # TODO: Lookup errors from credential instead of hardcoding them. + # Idea: Something like credential_errors(creds.PKGBASE_MERGE). + # Perhaps additionally: bad_credential_status_code(creds.PKGBASE_MERGE). + # 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."] + status_code = HTTPStatus.UNAUTHORIZED + + return render_template(request, "pkgbase/merge.html", context, + status_code=status_code) + + +@router.post("/pkgbase/{name}/merge") +@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())): + + pkgbase = get_pkg_or_base(name, PackageBase) + context = await make_variable_context(request, "Package Merging") + context["pkgbase"] = pkgbase + + # 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) + + 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) + + 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) + + 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) + + with db.begin(): + update_closure_comment(pkgbase, MERGE_ID, comments, target=target) + + # Merge pkgbase into target. + actions.pkgbase_merge_instance(request, pkgbase, target, comments=comments) + + if not next: + next = f"/pkgbase/{target.Name}" + + # Redirect to the newly merged into package. + return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py new file mode 100644 index 00000000..ac69f079 --- /dev/null +++ b/aurweb/routers/requests.py @@ -0,0 +1,91 @@ +from http import HTTPStatus + +from fastapi import APIRouter, Form, Query, Request +from fastapi.responses import RedirectResponse +from sqlalchemy import case + +from aurweb import db, defaults, time, util +from aurweb.auth import creds, requires_auth +from aurweb.models import PackageRequest, User +from aurweb.models.package_request import PENDING_ID, REJECTED_ID +from aurweb.requests.util import get_pkgreq_by_id +from aurweb.scripts import notify +from aurweb.templates import make_context, render_template + +router = APIRouter() + + +@router.get("/requests") +@requires_auth +async def requests(request: Request, + O: int = Query(default=defaults.O), + PP: int = Query(default=defaults.PP)): + context = make_context(request, "Requests") + + context["q"] = dict(request.query_params) + + O, PP = util.sanitize_params(O, PP) + context["O"] = O + context["PP"] = PP + + # A PackageRequest query, with left inner joined User and RequestType. + query = db.query(PackageRequest).join( + User, User.ID == PackageRequest.UsersID) + + # 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() + + 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 '/'. + return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) + + context = make_context(request, "Close Request") + context["pkgreq"] = pkgreq + return render_template(request, "requests/close.html", context) + + +@router.post("/requests/{id}/close") +@requires_auth +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. + approved = [pkgreq.User] + if not request.user.has_credential(creds.PKGREQ_CLOSE, approved=approved): + # Request user doesn't have permission here: redirect to '/'. + return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) + + context = make_context(request, "Close Request") + context["pkgreq"] = pkgreq + + now = time.utcnow() + with db.begin(): + pkgreq.Closer = request.user + pkgreq.ClosureComment = comments + pkgreq.ClosedTS = now + pkgreq.Status = REJECTED_ID + + notify_ = notify.RequestCloseNotification( + 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 new file mode 100644 index 00000000..942f4e9d --- /dev/null +++ b/aurweb/routers/rpc.py @@ -0,0 +1,127 @@ +import hashlib +import re + +from http import HTTPStatus +from typing import List, Optional +from urllib.parse import unquote + +import orjson + +from fastapi import APIRouter, Query, Request, Response +from fastapi.responses import JSONResponse + +from aurweb import defaults +from aurweb.ratelimit import check_ratelimit +from aurweb.rpc import RPC, documentation + +router = APIRouter() + + +def parse_args(request: Request): + """ 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 + of any more 'arg' or 'arg[]' parameters supplied before it. + + When 'arg[]' appears as the last argument given to the query string, + we iterate from last to first and build a list of arguments until + we hit an 'arg'. + + TODO: This handling should be addressed in v6 of the RPC API. This + was most likely a bi-product of legacy handling of versions 1-4 + which we no longer support. + + :param request: FastAPI request + :returns: List of deduced arguments + """ + # 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[]=")) + ] + + args = [] + if parts: + # If we found 'arg' and/or 'arg[]' arguments, we begin processing + # the set of arguments depending on the last key found. + last = parts[0][0] + + if last == "arg": + # If the last key was 'arg', then it is our sole argument. + args.append(parts[0][1]) + else: + # Otherwise, it must be 'arg[]', so traverse backward + # until we reach a non-'arg[]' key. + for key, value in parts: + if key != last: + break + args.append(value) + + return args + + +JSONP_EXPR = re.compile(r'^[a-zA-Z0-9()_.]{1,128}$') + + +@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)): + + if not request.url.query: + return documentation() + + # 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)) + + # If `callback` was provided, produce a text/javascript response + # valid for the jsonp callback. Otherwise, by default, return + # application/json containing `output`. + content_type = "application/json" + if callback: + if not re.match(JSONP_EXPR, callback): + return rpc.error("Invalid callback name.") + + content_type = "text/javascript" + + # Prepare list of arguments for input. If 'arg' was given, it'll + # be a list with one element. + arguments = parse_args(request) + data = rpc.handle(by=by, args=arguments) + + # Serialize `data` into JSON in a sorted fashion. This way, our + # ETag header produced below will never end up changed. + content = orjson.dumps(data, option=orjson.OPT_SORT_KEYS) + + # Produce an md5 hash based on `output`. + md5 = hashlib.md5() + md5.update(content) + etag = md5.hexdigest() + + # 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}"' + } + + 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 callback: + content = f"/**/{callback}({content.decode()})" + + return Response(content, headers=headers) diff --git a/aurweb/routers/rss.py b/aurweb/routers/rss.py new file mode 100644 index 00000000..454ff497 --- /dev/null +++ b/aurweb/routers/rss.py @@ -0,0 +1,83 @@ +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.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. + + :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 + """ + + feed = FeedGenerator() + feed.title("AUR Newest Packages") + feed.description("The latest and greatest packages in the AUR") + 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}/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.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}") + + 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") + + 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") + + 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 new file mode 100644 index 00000000..eff1c63f --- /dev/null +++ b/aurweb/routers/sso.py @@ -0,0 +1,172 @@ +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 +from sqlalchemy.sql import select +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 + +router = fastapi.APIRouter() + +oauth = OAuth() +oauth.register( + name="sso", + server_metadata_url=aurweb.config.get("sso", "openid_configuration"), + client_kwargs={"scope": "openid"}, + client_id=aurweb.config.get("sso", "client_id"), + client_secret=aurweb.config.get("sso", "client_secret"), +) + + +@router.get("/sso/login") +async def login(request: Request, redirect: str = None): + """ + Redirect the user to the SSO provider’s login page. + + We specify prompt=login to force the user to input their credentials even + if they’re already logged on the SSO. This is less practical, but given AUR + has the potential to impact many users, better safe than sorry. + + The `redirect` argument is a query parameter specifying the post-login + redirect URL. + """ + 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() + return row is not None and bool(row[0]) + + +def open_session(request, conn, user_id): + """ + Create a new user session into the database. Return its SID. + """ + if is_account_suspended(conn, user_id): + _ = get_translator_for_request(request) + 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(), + )) + + # 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)) + + return sid + + +def is_ip_banned(conn, ip): + """ + Check if an IP is banned. `ip` is a string and may be an IPv4 as well as an + IPv6, depending on the server’s configuration. + """ + result = conn.execute(Bans.select().where(Bans.c.IPAddress == ip)) + return result.fetchone() is not None + + +def is_aur_url(url): + aur_location = aurweb.config.get("options", "aur_location") + if not aur_location.endswith("/"): + aur_location = aur_location + "/" + return url.startswith(aur_location) + + +@router.get("/sso/authenticate") +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): + _ = 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.')) + + try: + token = await oauth.sso.authorize_access_token(request) + user = await oauth.sso.parse_id_token(request, token) + except OAuthError: + # Here, most OAuth errors should be caused by forged or expired tokens. + # Let’s give attackers as little information as possible. + _ = get_translator_for_request(request) + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + 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.")) + + 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 "/") + secure_cookies = aurweb.config.getboolean("options", "disable_http_login") + 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) + return util.add_samesite_fields(response, "strict") + else: + # We’ve got a severe integrity violation. + raise Exception("Multiple accounts found for SSO account " + sub) + + +@router.get("/sso/logout") +async def logout(request: Request): + """ + Disconnect the user from the SSO provider, potentially affecting every + other Arch service. AUR logout is performed by `/logout`, before it + redirects to `/sso/logout`. + + Based on the OpenID Connect Session Management specification: + https://openid.net/specs/openid-connect-session-1_0.html#RPLogout + """ + id_token = request.cookies.get("SSO_ID_TOKEN") + if not id_token: + 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) + response.delete_cookie("SSO_ID_TOKEN", path="/sso/") + return response diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py new file mode 100644 index 00000000..07ee17af --- /dev/null +++ b/aurweb/routers/trusted_user.py @@ -0,0 +1,311 @@ +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_, or_ + +from aurweb import db, l10n, logging, models, time +from aurweb.auth import creds, requires_auth +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 + + # TODO + # We order last votes by TUVote.VoteID and User.Username. + # This is really bad. We should add a Created column to + # TUVote of type Timestamp and order by that instead. + last_votes_by_tu = db.query(models.TUVote).filter( + and_(models.TUVote.VoteID == models.TUVoteInfo.ID, + models.TUVoteInfo.End <= ts, + models.TUVote.UserID == models.User.ID, + or_(models.User.AccountTypeID == 2, + models.User.AccountTypeID == 4)) + ).group_by(models.User.ID).order_by( + models.TUVote.VoteID.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 total 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}") +@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) + voteinfo.ActiveTUs += 1 + + 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") +@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) + + voteinfo = db.query(models.TUVoteInfo).filter( + models.TUVoteInfo.User == user).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 new file mode 100644 index 00000000..90e03a41 --- /dev/null +++ b/aurweb/rpc.py @@ -0,0 +1,389 @@ +import os + +from collections import defaultdict +from typing import Any, Callable, Dict, List, 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.exceptions import RPCError +from aurweb.filters import number_format +from aurweb.packages.search import RPCSearch + +TYPE_MAPPING = { + "depends": "Depends", + "makedepends": "MakeDepends", + "checkdepends": "CheckDepends", + "optdepends": "OptDepends", + "conflicts": "Conflicts", + "provides": "Provides", + "replaces": "Replaces", +} + +DataGenerator = NewType("DataGenerator", + Callable[[models.Package], Dict[str, Any]]) + + +def documentation(): + aurwebdir = config.get("options", "aurwebdir") + rpc_doc = os.path.join(aurwebdir, "doc", "rpc.html") + + if not os.path.exists(rpc_doc): + raise OSError("doc/rpc.html could not be read") + + with open(rpc_doc) as f: + data = f.read() + return HTMLResponse(data) + + +class RPC: + """ 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 + RPC implementation to achieve goals. We call type handlers + by taking a reference to the callback named "_handle_{type}_type(...)", + and if the handler does not exist, we return a not implemented + error to the API user. + + EXPOSED_VERSIONS holds the set of versions that the API + officially supports. + + EXPOSED_TYPES holds the set of types that the API officially + supports. + + ALIASES holds an alias mapping of type -> type strings. + + We should focus on privatizing implementation helpers and + focusing on performance in the code used. + """ + + # A set of RPC versions supported by this API. + EXPOSED_VERSIONS = {5} + + # A set of RPC types supported by this API. + EXPOSED_TYPES = { + "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" + } + + # A mapping of by aliases. + BY_ALIASES = {"name-desc": "nd", "name": "n", "maintainer": "m"} + + 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]: + return { + "version": self.version, + "results": [], + "resultcount": 0, + "type": "error", + "error": message + } + + def _verify_inputs(self, by: str = [], args: List[str] = []) -> None: + if self.version is None: + raise RPCError("Please specify an API version.") + + if self.version not in RPC.EXPOSED_VERSIONS: + raise RPCError("Invalid version specified.") + + if by not in RPC.EXPOSED_BYS: + raise RPCError("Incorrect by field specified.") + + if self.type is None: + raise RPCError("No request type/data specified.") + + if self.type not in RPC.EXPOSED_TYPES: + raise RPCError("Incorrect request type specified.") + + 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. + + :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 + pop = 0 if not pop else float(number_format(pop, 6)) + + snapshot_uri = config.get("options", "snapshot_uri") + return { + "ID": package.ID, + "Name": package.Name, + "PackageBaseID": package.PackageBaseID, + "PackageBase": package.PackageBaseName, + # Maintainer should be set following this update if one exists. + "Maintainer": package.Maintainer, + "Version": package.Version, + "Description": package.Description, + "URL": package.URL, + "URLPath": snapshot_uri % package.Name, + "NumVotes": package.NumVotes, + "Popularity": pop, + "OutOfDate": package.OutOfDateTS, + "FirstSubmitted": package.SubmittedTS, + "LastModified": package.ModifiedTS + } + + 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": [] + }) + + # If we actually got extra_info records, update data with + # them for this particular package. + if self.extra_info: + data.update(self.extra_info.get(package.ID, {})) + + return data + + 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. + + :param packages: A list of Package instances or a Package ORM query + :param data_generator: Generator callable of single-Package JSON data + """ + 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 _handle_multiinfo_type(self, args: List[str] = [], **kwargs) \ + -> List[Dict[str, Any]]: + self._enforce_args(args) + args = set(args) + + 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)) + packages = self._entities(packages) + + ids = {pkg.ID for pkg in packages} + + # Aliases for 80-width. + Package = models.Package + PackageKeyword = models.PackageKeyword + + subqueries = [ + # PackageDependency + 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"), + + # PackageRelation + 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"), + + # Groups + db.query(models.PackageGroup).join( + models.Group, + 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"), + + # Licenses + 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"), + + # Keywords + db.query(models.PackageKeyword).join( + models.Package, + 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") + ] + + # Union all subqueries together. + query = subqueries[0].union_all(*subqueries[1:]) + + # Store our extra information in a class-wise dictionary, + # which contains package id -> extra info dict mappings. + self.extra_info = defaultdict(lambda: defaultdict(list)) + for record in query: + type_ = TYPE_MAPPING.get(record.Type, record.Type) + + name = record.Name + if record.Cond: + name += record.Cond + + self.extra_info[record.ID][type_].append(name) + + 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. + if by != "m" and not len(args): + raise RPCError("No request type/data specified.") + + arg = args[0] if args else str() + if by != "m" and len(arg) < 2: + raise RPCError("Query arg too small.") + + search = RPCSearch() + search.search_by(by, arg) + + max_results = config.getint("options", "max_rpc_results") + results = self._entities(search.results()).limit(max_results) + return self._assemble_json_data(results, self._get_json_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]: + 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) + return [pkg.Name for pkg in packages] + + def _handle_suggest_pkgbase_type(self, args: List[str] = [], **kwargs)\ + -> List[str]: + if not args: + return [] + + packages = db.query(models.PackageBase.Name).filter( + and_(models.PackageBase.PackagerUID.isnot(None), + models.PackageBase.Name.like(f"%{args[0]}%")) + ).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]]: + # 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 + to this function and expect an output dictionary to be returned. + + :param v: RPC version argument + :param type: RPC type argument + :param args: Deciphered list of arguments based on arg/arg[] inputs + """ + # Prepare our output data dictionary with some basic keys. + data = {"version": self.version, "type": self.type} + + # Run some verification on our given arguments. + try: + self._verify_inputs(by=by, args=args) + except RPCError as exc: + return self.error(str(exc)) + + # Convert by to its aliased value if it has one. + by = RPC.BY_ALIASES.get(by, by) + + # Process the requested handler. + try: + results = self._handle_callback(by, args) + except RPCError as exc: + return self.error(str(exc)) + + # These types are special: we produce a different kind of + # successful JSON output: a list of results. + if self._is_suggestion(): + return results + + # Return JSON output. + data.update({ + "resultcount": len(results), + "results": results + }) + return data diff --git a/aurweb/schema.py b/aurweb/schema.py index 8f1e2b3a..39550ff6 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -6,7 +6,7 @@ usually be automatically generated. See `migrations/README` for details. """ -from sqlalchemy import CHAR, Column, ForeignKey, Index, MetaData, String, TIMESTAMP, 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 @@ -16,19 +16,19 @@ db_backend = aurweb.config.get("database", "backend") @compiles(TINYINT, 'sqlite') -def compile_tinyint_sqlite(type_, compiler, **kw): +def compile_tinyint_sqlite(type_, compiler, **kw): # pragma: no cover """TINYINT is not supported on SQLite. Substitute it with INTEGER.""" return 'INTEGER' @compiles(BIGINT, 'sqlite') -def compile_bigint_sqlite(type_, compiler, **kw): +def compile_bigint_sqlite(type_, compiler, **kw): # pragma: no cover """ For SQLite's AUTOINCREMENT to work on BIGINT columns, we need to map BIGINT to INTEGER. Aside from that, BIGINT is the same as INTEGER for SQLite. See https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#allowing-autoincrement-behavior-sqlalchemy-types-other-than-integer-integer - """ + """ # noqa: E501 return 'INTEGER' @@ -107,7 +107,10 @@ PackageBases = Table( 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), 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), @@ -130,7 +133,7 @@ PackageBases = Table( # Keywords of package bases PackageKeywords = Table( 'PackageKeywords', metadata, - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + 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', @@ -167,8 +170,8 @@ Licenses = Table( # Information about package-license-relations PackageLicenses = Table( 'PackageLicenses', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=False), - Column('LicenseID', ForeignKey('Licenses.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + 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', ) @@ -187,8 +190,8 @@ Groups = Table( # Information about package-group-relations PackageGroups = Table( 'PackageGroups', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=False), - Column('GroupID', ForeignKey('Groups.ID', ondelete='CASCADE'), primary_key=True, nullable=False), + 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', ) @@ -383,12 +386,15 @@ TU_VoteInfo = Table( 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), 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', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), - Column('No', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), - Column('Abstain', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), - Column('ActiveTUs', TINYINT(3, unsigned=True), nullable=False, server_default=text("'0'")), + 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', @@ -441,7 +447,7 @@ AcceptedTerms = Table( # Rate limits for API ApiRateLimit = Table( 'ApiRateLimit', metadata, - Column('IP', String(45), primary_key=True), + 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'), diff --git a/aurweb/scripts/adduser.py b/aurweb/scripts/adduser.py new file mode 100644 index 00000000..4cc059d1 --- /dev/null +++ b/aurweb/scripts/adduser.py @@ -0,0 +1,74 @@ +""" +Add a user to the configured aurweb database. + +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 +from aurweb.models.user import User + + +def parse_args(): + parser = argparse.ArgumentParser(description="aurweb-adduser options") + + parser.add_argument("-u", "--username", help="Username", required=True) + parser.add_argument("-e", "--email", help="Email", required=True) + parser.add_argument("-p", "--password", help="Password", required=True) + parser.add_argument("-r", "--realname", help="Real Name") + parser.add_argument("-i", "--ircnick", help="IRC Nick") + parser.add_argument("--pgp-key", help="PGP Key Fingerprint") + 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) + + return parser.parse_args() + + +def main(): + args = parse_args() + + db.get_engine() + 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) + + if args.ssh_pubkey: + pubkey = args.ssh_pubkey.strip() + + # Remove host from the pubkey if it's there. + pubkey = ' '.join(pubkey.split(' ')[:2]) + + with db.begin(): + db.create(SSHPubKey, + User=user, + PubKey=pubkey, + Fingerprint=get_fingerprint(pubkey)) + + print(user.json()) + return 0 + + +if __name__ == "__main__": + e = 1 + try: + e = main() + except Exception: + traceback.print_exc() + e = 1 + sys.exit(e) diff --git a/aurweb/scripts/aurblup.py b/aurweb/scripts/aurblup.py index a7d43f12..9c9059ec 100755 --- a/aurweb/scripts/aurblup.py +++ b/aurweb/scripts/aurblup.py @@ -1,32 +1,37 @@ #!/usr/bin/env python3 -import pyalpm import re +import pyalpm + +from sqlalchemy import and_ + import aurweb.config -import aurweb.db -db_path = aurweb.config.get('aurblup', 'db-path') -sync_dbs = aurweb.config.get('aurblup', 'sync-dbs').split(' ') -server = aurweb.config.get('aurblup', 'server') +from aurweb import db, util +from aurweb.models import OfficialProvider -def main(): +def _main(force: bool = False): blacklist = set() providers = set() 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') + h = pyalpm.Handle("/", db_path) for sync_db in sync_dbs: repo = h.register_syncdb(sync_db, pyalpm.SIG_DATABASE_OPTIONAL) repo.servers = [server.replace("%s", sync_db)] t = h.init_transaction() - repo.update(False) + repo.update(force) t.release() for pkg in repo.pkgcache: blacklist.add(pkg.name) - [blacklist.add(x) for x in pkg.replaces] + util.apply_all(pkg.replaces, blacklist.add) providers.add((pkg.name, pkg.name)) repomap[(pkg.name, pkg.name)] = repo.name for provision in pkg.provides: @@ -34,21 +39,29 @@ def main(): providers.add((pkg.name, provisionname)) repomap[(pkg.name, provisionname)] = repo.name - conn = aurweb.db.Connection() + with db.begin(): + old_providers = set( + db.query(OfficialProvider).with_entities( + OfficialProvider.Name.label("Name"), + OfficialProvider.Provides.label("Provides") + ).distinct().order_by("Name").all() + ) - cur = conn.execute("SELECT Name, Provides FROM OfficialProviders") - oldproviders = set(cur.fetchall()) + for name, provides in old_providers.difference(providers): + db.delete_all(db.query(OfficialProvider).filter( + and_(OfficialProvider.Name == name, + OfficialProvider.Provides == provides) + )) - for pkg, provides in oldproviders.difference(providers): - conn.execute("DELETE FROM OfficialProviders " - "WHERE Name = ? AND Provides = ?", [pkg, provides]) - for pkg, provides in providers.difference(oldproviders): - repo = repomap[(pkg, provides)] - conn.execute("INSERT INTO OfficialProviders (Name, Repo, Provides) " - "VALUES (?, ?, ?)", [pkg, repo, provides]) + for name, provides in providers.difference(old_providers): + repo = repomap.get((name, provides)) + db.create(OfficialProvider, Name=name, + Repo=repo, Provides=provides) - conn.commit() - conn.close() + +def main(force: bool = False): + db.get_engine() + _main(force) if __name__ == '__main__': diff --git a/aurweb/scripts/config.py b/aurweb/scripts/config.py new file mode 100644 index 00000000..e7c91dd1 --- /dev/null +++ b/aurweb/scripts/config.py @@ -0,0 +1,69 @@ +""" +Perform an action on the aurweb config. + +When AUR_CONFIG_IMMUTABLE is set, the `set` action is noop. +""" +import argparse +import configparser +import os +import sys + +import aurweb.config + + +def do_action(func, *args, save: bool = True): + # If AUR_CONFIG_IMMUTABLE is defined, skip out on config setting. + if int(os.environ.get("AUR_CONFIG_IMMUTABLE", 0)): + return + + value = None + try: + value = func(*args) + if save: + aurweb.config.save() + except configparser.NoSectionError: + print("error: no section found", file=sys.stderr) + except configparser.NoOptionError: + print("error: no option found", file=sys.stderr) + + return value + + +def action_set(args): + if not args.value: + print("error: no value provided", file=sys.stderr) + return + do_action(aurweb.config.set_option, args.section, args.option, args.value) + + +def action_unset(args): + do_action(aurweb.config.unset_option, args.section, args.option) + + +def action_get(args): + val = do_action(aurweb.config.get, args.section, args.option, save=False) + print(val) + + +def parse_args(): + fmt_cls = argparse.RawDescriptionHelpFormatter + actions = ["get", "set", "unset"] + parser = argparse.ArgumentParser( + description="aurweb configuration tool", + 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") + return parser.parse_args() + + +def main(): + args = parse_args() + action = getattr(sys.modules[__name__], f"action_{args.action}") + return action(args) + + +if __name__ == "__main__": + main() diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index afa5fb5c..d97bc01c 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -18,24 +18,33 @@ on the following, right-hand side fields are added to each item. """ -import datetime import gzip +import os import sys + from collections import defaultdict -from decimal import Decimal +from typing import Any, Dict import orjson +from sqlalchemy import literal, orm + import aurweb.config -import aurweb.db -packagesfile = aurweb.config.get('mkpkglists', 'packagesfile') -packagesmetafile = aurweb.config.get('mkpkglists', 'packagesmetafile') -packagesmetaextfile = aurweb.config.get('mkpkglists', 'packagesmetaextfile') +from aurweb import db, filters, logging, models, util +from aurweb.benchmark import Benchmark +from aurweb.models import Package, PackageBase, User -pkgbasefile = aurweb.config.get('mkpkglists', 'pkgbasefile') +logger = logging.get_logger("aurweb.scripts.mkpkglists") -userfile = aurweb.config.get('mkpkglists', 'userfile') +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') TYPE_MAP = { @@ -49,7 +58,7 @@ TYPE_MAP = { } -def get_extended_dict(query: str): +def get_extended_dict(query: orm.Query): """ Produce data in the form in a single bulk SQL query: @@ -70,61 +79,75 @@ def get_extended_dict(query: str): output[i].update(data.get(package_id)) """ - conn = aurweb.db.Connection() - - cursor = conn.execute(query) - data = defaultdict(lambda: defaultdict(list)) - for result in cursor.fetchall(): - + for result in query: pkgid = result[0] key = TYPE_MAP.get(result[1], result[1]) output = result[2] if result[3]: output += result[3] - - # In all cases, we have at least an empty License list. - if "License" not in data[pkgid]: - data[pkgid]["License"] = [] - - # In all cases, we have at least an empty Keywords list. - if "Keywords" not in data[pkgid]: - data[pkgid]["Keywords"] = [] - data[pkgid][key].append(output) - conn.close() return data def get_extended_fields(): - # Returns: [ID, Type, Name, Cond] - query = """ - SELECT PackageDepends.PackageID AS ID, DependencyTypes.Name AS Type, - PackageDepends.DepName AS Name, PackageDepends.DepCondition AS Cond - FROM PackageDepends - LEFT JOIN DependencyTypes - ON DependencyTypes.ID = PackageDepends.DepTypeID - UNION SELECT PackageRelations.PackageID AS ID, RelationTypes.Name AS Type, - PackageRelations.RelName AS Name, - PackageRelations.RelCondition AS Cond - FROM PackageRelations - LEFT JOIN RelationTypes - ON RelationTypes.ID = PackageRelations.RelTypeID - UNION SELECT PackageGroups.PackageID AS ID, 'Groups' AS Type, - Groups.Name, '' AS Cond - FROM Groups - INNER JOIN PackageGroups ON PackageGroups.GroupID = Groups.ID - UNION SELECT PackageLicenses.PackageID AS ID, 'License' AS Type, - Licenses.Name, '' as Cond - FROM Licenses - INNER JOIN PackageLicenses ON PackageLicenses.LicenseID = Licenses.ID - UNION SELECT Packages.ID AS ID, 'Keywords' AS Type, - PackageKeywords.Keyword AS Name, '' as Cond - FROM PackageKeywords - INNER JOIN Packages ON Packages.PackageBaseID = PackageKeywords.PackageBaseID - """ + subqueries = [ + # PackageDependency + 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"), + + # PackageRelation + 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"), + + # Groups + 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"), + + # Licenses + 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"), + + # Keywords + 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") + ] + query = subqueries[0].union_all(*subqueries[1:]) return get_extended_dict(query) @@ -133,97 +156,122 @@ EXTENDED_FIELD_HANDLERS = { } -def is_decimal(column): - """ Check if an SQL column is of decimal.Decimal type. """ - if isinstance(column, Decimal): - return float(column) - return column +def as_dict(package: Package) -> Dict[str, Any]: + return { + "ID": package.ID, + "Name": package.Name, + "PackageBaseID": package.PackageBaseID, + "PackageBase": package.PackageBase, + "Version": package.Version, + "Description": package.Description, + "NumVotes": package.NumVotes, + "Popularity": float(package.Popularity), + "OutOfDate": package.OutOfDate, + "Maintainer": package.Maintainer, + "FirstSubmitted": package.FirstSubmitted, + "LastModified": package.LastModified, + } -def write_archive(archive: str, output: list): - with gzip.open(archive, "wb") as f: - f.write(b"[\n") - for i, item in enumerate(output): - f.write(orjson.dumps(item)) - if i < len(output) - 1: - f.write(b",") - f.write(b"\n") - f.write(b"]") +def _main(): + bench = Benchmark() + logger.info("Started re-creating archives, wait a while...") - -def main(): - conn = aurweb.db.Connection() - - datestr = datetime.datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S GMT") - pkglist_header = "# AUR package list, generated on " + datestr - pkgbaselist_header = "# AUR package base list, generated on " + datestr - userlist_header = "# AUR user name list, generated on " + datestr - - # Query columns; copied from RPC. - columns = ("Packages.ID, Packages.Name, " - "PackageBases.ID AS PackageBaseID, " - "PackageBases.Name AS PackageBase, " - "Version, Description, URL, NumVotes, " - "Popularity, OutOfDateTS AS OutOfDate, " - "Users.UserName AS Maintainer, " - "SubmittedTS AS FirstSubmitted, " - "ModifiedTS AS LastModified") - - # Perform query. - cur = conn.execute(f"SELECT {columns} FROM Packages " - "LEFT JOIN PackageBases " - "ON PackageBases.ID = Packages.PackageBaseID " - "LEFT JOIN Users " - "ON PackageBases.MaintainerUID = Users.ID " - "WHERE PackageBases.PackagerUID IS NOT NULL") + 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, + PackageBase.NumVotes, + PackageBase.Popularity, + PackageBase.OutOfDateTS.label("OutOfDate"), + User.Username.label("Maintainer"), + PackageBase.SubmittedTS.label("FirstSubmitted"), + PackageBase.ModifiedTS.label("LastModified") + ).distinct().order_by("Name") # Produce packages-meta-v1.json.gz output = list() snapshot_uri = aurweb.config.get("options", "snapshot_uri") - for result in cur.fetchall(): - item = { - column[0]: is_decimal(result[i]) - for i, column in enumerate(cur.description) - } - item["URLPath"] = snapshot_uri % item.get("Name") - output.append(item) + gzips = { + "packages": gzip.open(PACKAGES, "wt"), + "meta": gzip.open(META, "wb"), + } - write_archive(packagesmetafile, output) + # Append list opening to the metafile. + gzips["meta"].write(b"[\n") - # Produce packages-meta-ext-v1.json.gz + # 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(META_EXT, "wb") + # Append list opening to the meta_ext file. + gzips.get("meta_ext").write(b"[\n") f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) data = f() + extended = True - default_ = {"Groups": [], "License": [], "Keywords": []} - for i in range(len(output)): - data_ = data.get(output[i].get("ID"), default_) - output[i].update(data_) + 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") - write_archive(packagesmetaextfile, output) + # Construct our result JSON dictionary. + item = as_dict(result) + item["URLPath"] = snapshot_uri % result.Name - # Produce packages.gz - with gzip.open(packagesfile, "wb") as f: - f.write(bytes(pkglist_header + "\n", "UTF-8")) - f.writelines([ - bytes(x.get("Name") + "\n", "UTF-8") - for x in output - ]) + # 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) + + 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"]") + if extended: + gzips.get("meta_ext").write(b"]") + + # Close gzip files. + util.apply_all(gzips.values(), lambda gz: gz.close()) # Produce pkgbase.gz - with gzip.open(pkgbasefile, "w") as f: - f.write(bytes(pkgbaselist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT Name FROM PackageBases " + - "WHERE PackagerUID IS NOT NULL") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + query = db.query(PackageBase.Name).filter( + PackageBase.PackagerUID.isnot(None)).all() + with gzip.open(PKGBASE, "wt") as f: + f.writelines([f"{base.Name}\n" for i, base in enumerate(query)]) # Produce users.gz - with gzip.open(userfile, "w") as f: - f.write(bytes(userlist_header + "\n", "UTF-8")) - cur = conn.execute("SELECT UserName FROM Users") - f.writelines([bytes(x[0] + "\n", "UTF-8") for x in cur.fetchall()]) + query = db.query(User.Username).all() + with gzip.open(USERS, "wt") as f: + f.writelines([f"{user.Username}\n" for i, user in enumerate(query)]) - conn.close() + seconds = filters.number_format(bench.end(), 4) + logger.info(f"Completed in {seconds} seconds.") + + +def main(): + db.get_engine() + with db.begin(): + _main() if __name__ == '__main__': diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 2bf5555c..bc06085c 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -7,10 +7,26 @@ import subprocess import sys import textwrap +from typing import List, Tuple + +from sqlalchemy import and_, or_ + import aurweb.config import aurweb.db +import aurweb.filters import aurweb.l10n +from aurweb import db, l10n, logging +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 + +logger = logging.get_logger(__name__) + aur_location = aurweb.config.get('options', 'aur_location') @@ -22,27 +38,7 @@ def headers_reply(thread_id): return {'In-Reply-To': thread_id, 'References': thread_id} -def username_from_id(conn, uid): - cur = conn.execute('SELECT UserName FROM Users WHERE ID = ?', [uid]) - return cur.fetchone()[0] - - -def pkgbase_from_id(conn, pkgbase_id): - cur = conn.execute('SELECT Name FROM PackageBases WHERE ID = ?', - [pkgbase_id]) - return cur.fetchone()[0] - - -def pkgbase_from_pkgreq(conn, reqid): - cur = conn.execute('SELECT PackageBaseID FROM PackageRequests ' + - 'WHERE ID = ?', [reqid]) - return cur.fetchone()[0] - - class Notification: - def __init__(self): - self._l10n = aurweb.l10n.Translator() - def get_refs(self): return () @@ -55,15 +51,15 @@ class Notification: def get_body_fmt(self, lang): 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' for i, ref in enumerate(self.get_refs()): body += '\n' + '[%d] %s' % (i + 1, ref) return body.rstrip() - def send(self): + def _send(self) -> None: sendmail = aurweb.config.get('notifications', 'sendmail') sender = aurweb.config.get('notifications', 'sender') reply_to = aurweb.config.get('notifications', 'reply-to') @@ -97,16 +93,20 @@ class Notification: 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') + 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') - if use_ssl: - server = smtplib.SMTP_SSL(server_addr, server_port) - else: - server = smtplib.SMTP(server_addr, server_port) + classes = { + False: smtplib.SMTP, + True: smtplib.SMTP_SSL, + } + server = classes[use_ssl](server_addr, server_port) if use_starttls: server.ehlo() @@ -121,13 +121,76 @@ class Notification: server.sendmail(sender, deliver_to, msg.as_bytes()) server.quit() + def send(self) -> None: + try: + self._send() + except OSError as exc: + logger.error("Unable to emit notification due to an " + "OSError (precise exception following).") + logger.error(str(exc)) + + +class ServerErrorNotification(Notification): + """ A notification used to represent an internal server error. """ + + def __init__(self, traceback_id: int, version: str, utc: int): + """ + Construct a ServerErrorNotification. + + :param traceback_id: Traceback ID + :param version: aurweb version + :param utc: UTC timestamp + """ + self._tb_id = traceback_id + self._version = version + self._utc = utc + + postmaster = aurweb.config.get("notifications", "postmaster") + self._to = postmaster + + super().__init__() + + def get_recipients(self) -> List[Tuple[str, str]]: + from aurweb.auth import AnonymousUser + user = (db.query(User).filter(User.Email == self._to).first() + or AnonymousUser()) + return [(self._to, user.LangPreference)] + + def get_subject(self, lang: str) -> str: + return l10n.translator.translate("AUR Server Error", lang) + + def get_body(self, lang: str) -> str: + """ A forcibly English email body. """ + dt = aurweb.filters.timestamp_to_datetime(self._utc) + dts = dt.strftime("%Y-%m-%d %H:%M") + return (f"Traceback ID: {self._tb_id}\n" + f"Location: {aur_location}\n" + f"Version: {self._version}\n" + f"Datetime: {dts} UTC\n") + + def get_refs(self): + return (aur_location,) + class ResetKeyNotification(Notification): - def __init__(self, conn, uid): - cur = conn.execute('SELECT UserName, Email, BackupEmail, ' + - 'LangPreference, ResetKey ' + - 'FROM Users WHERE ID = ?', [uid]) - self._username, self._to, self._backup, self._lang, self._resetkey = cur.fetchone() + 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() + + self._username = user.Username + self._to = user.Email + self._backup = user.BackupEmail + self._lang = user.LangPreference + self._resetkey = user.ResetKey + super().__init__() def get_recipients(self): @@ -137,15 +200,15 @@ class ResetKeyNotification(Notification): return [(self._to, self._lang)] def get_subject(self, lang): - return self._l10n.translate('AUR Password Reset', lang) + return aurweb.l10n.translator.translate('AUR Password Reset', lang) def get_body(self, lang): - return self._l10n.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) + 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) def get_refs(self): return (aur_location + '/passreset/?resetkey=' + self._resetkey,) @@ -153,51 +216,62 @@ class ResetKeyNotification(Notification): class WelcomeNotification(ResetKeyNotification): def get_subject(self, lang): - return self._l10n.translate('Welcome to the Arch User Repository', - lang) + return aurweb.l10n.translator.translate( + 'Welcome to the Arch User Repository', + lang) def get_body(self, lang): - return self._l10n.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) + 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) class CommentNotification(Notification): - def __init__(self, conn, uid, pkgbase_id, comment_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, Users.LangPreference ' - 'FROM Users INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'Users.CommentNotify = 1 AND ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', - [uid, pkgbase_id]) - self._recipients = cur.fetchall() - cur = conn.execute('SELECT Comments FROM PackageComments WHERE ID = ?', - [comment_id]) - self._text = cur.fetchone()[0] + 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 + + 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() + self._text = pkgcomment.Comments + super().__init__() def get_recipients(self): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Comment for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Comment for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_body(self, lang): - body = self._l10n.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 = self._l10n.translate('Disable notifications', lang) - body += self._l10n.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) + 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) + 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) return body def get_refs(self): @@ -211,38 +285,45 @@ class CommentNotification(Notification): class UpdateNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'Users.UpdateNotify = 1 AND ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', - [uid, pkgbase_id]) - self._recipients = cur.fetchall() + 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 + + 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__() def get_recipients(self): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Package Update: {pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Package Update: {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_body(self, lang): - body = self._l10n.translate('{user} [1] pushed a new commit to ' - '{pkgbase} [2].', lang).format( - user=self._user, - pkgbase=self._pkgbase) - body += '\n\n-- \n' - dnlabel = self._l10n.translate('Disable notifications', lang) - body += self._l10n.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) + 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) + 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) return body def get_refs(self): @@ -256,36 +337,45 @@ class UpdateNotification(Notification): class FlagNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'LEFT JOIN PackageComaintainers ' + - 'ON PackageComaintainers.UsersID = Users.ID ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.MaintainerUID = Users.ID OR ' + - 'PackageBases.ID = PackageComaintainers.PackageBaseID ' + - 'WHERE PackageBases.ID = ?', [pkgbase_id]) - self._recipients = cur.fetchall() - cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' + - 'ID = ?', [pkgbase_id]) - self._text = cur.fetchone()[0] + 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 + + 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() + self._recipients = [(u.Email, u.LangPreference) for u in query] + + pkgbase = db.query(PackageBase.FlaggerComment).filter( + PackageBase.ID == pkgbase_id).first() + self._text = pkgbase.FlaggerComment + super().__init__() def get_recipients(self): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Out-of-date Notification for ' - '{pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Out-of-date Notification for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_body(self, lang): - body = self._l10n.translate( - 'Your package {pkgbase} [1] has been flagged out-of-date by ' - '{user} [2]:', lang).format(pkgbase=self._pkgbase, - user=self._user) + 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 return body @@ -295,29 +385,37 @@ class FlagNotification(Notification): class OwnershipEventNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'Users.OwnershipNotify = 1 AND ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', - [uid, pkgbase_id]) - self._recipients = cur.fetchall() - cur = conn.execute('SELECT FlaggerComment FROM PackageBases WHERE ' + - 'ID = ?', [pkgbase_id]) - self._text = cur.fetchone()[0] + 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 + + 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() + self._text = pkgbase.FlaggerComment + super().__init__() def get_recipients(self): return self._recipients def get_subject(self, lang): - return self._l10n.translate('AUR Ownership Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Ownership Notification for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_refs(self): return (aur_location + '/pkgbase/' + self._pkgbase + '/', @@ -326,34 +424,45 @@ class OwnershipEventNotification(Notification): class AdoptNotification(OwnershipEventNotification): def get_body(self, lang): - return self._l10n.translate( - 'The package {pkgbase} [1] was adopted by {user} [2].', - lang).format(pkgbase=self._pkgbase, user=self._user) + return aurweb.l10n.translator.translate( + '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 self._l10n.translate( - 'The package {pkgbase} [1] was disowned by {user} ' - '[2].', lang).format(pkgbase=self._pkgbase, - user=self._user) + return aurweb.l10n.translator.translate( + 'The package {pkgbase} [1] was disowned by {user} ' + '[2].', lang).format(pkgbase=self._pkgbase, + user=self._user) class ComaintainershipEventNotification(Notification): - def __init__(self, conn, uid, pkgbase_id): - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT Email, LangPreference FROM Users ' + - 'WHERE ID = ?', [uid]) - self._to, self._lang = cur.fetchone() + def __init__(self, uid, pkgbase_id): + + self._pkgbase = db.query(PackageBase.Name).filter( + PackageBase.ID == pkgbase_id).first().Name + + user = db.query(User).filter( + and_(User.ID == uid, + User.Suspended == 0) + ).with_entities( + User.Email, + User.LangPreference + ).first() + + self._to = user.Email + self._lang = user.LangPreference + super().__init__() def get_recipients(self): return [(self._to, self._lang)] def get_subject(self, lang): - return self._l10n.translate('AUR Co-Maintainer Notification for ' - '{pkgbase}', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'AUR Co-Maintainer Notification for {pkgbase}', + lang).format(pkgbase=self._pkgbase) def get_refs(self): return (aur_location + '/pkgbase/' + self._pkgbase + '/',) @@ -361,59 +470,68 @@ class ComaintainershipEventNotification(Notification): class ComaintainerAddNotification(ComaintainershipEventNotification): def get_body(self, lang): - return self._l10n.translate( - 'You were added to the co-maintainer list of {pkgbase} [1].', - lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + '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 self._l10n.translate( - 'You were removed from the co-maintainer list of {pkgbase} ' - '[1].', lang).format(pkgbase=self._pkgbase) + return aurweb.l10n.translator.translate( + 'You were removed from the co-maintainer list of {pkgbase} ' + '[1].', lang).format(pkgbase=self._pkgbase) class DeleteNotification(Notification): - def __init__(self, conn, uid, old_pkgbase_id, new_pkgbase_id=None): - self._user = username_from_id(conn, uid) - self._old_pkgbase = pkgbase_from_id(conn, old_pkgbase_id) + 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._new_pkgbase = None if new_pkgbase_id: - self._new_pkgbase = pkgbase_from_id(conn, new_pkgbase_id) - else: - self._new_pkgbase = None - cur = conn.execute('SELECT DISTINCT Users.Email, ' + - 'Users.LangPreference FROM Users ' + - 'INNER JOIN PackageNotifications ' + - 'ON PackageNotifications.UserID = Users.ID WHERE ' + - 'PackageNotifications.UserID != ? AND ' + - 'PackageNotifications.PackageBaseID = ?', - [uid, old_pkgbase_id]) - self._recipients = cur.fetchall() + 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 self._l10n.translate('AUR Package deleted: {pkgbase}', - lang).format(pkgbase=self._old_pkgbase) + 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 = self._l10n.translate('Disable notifications', lang) - return self._l10n.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) + 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 self._l10n.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) + 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 + '/', @@ -424,25 +542,36 @@ class DeleteNotification(Notification): class RequestOpenNotification(Notification): - def __init__(self, conn, uid, reqid, reqtype, pkgbase_id, merge_into=None): - self._user = username_from_id(conn, uid) - self._pkgbase = pkgbase_from_id(conn, pkgbase_id) - cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + - 'LEFT JOIN PackageComaintainers ' + - 'ON PackageComaintainers.PackageBaseID = PackageRequests.PackageBaseID ' + - 'INNER JOIN Users ' + - 'ON Users.ID = PackageRequests.UsersID ' + - 'OR Users.ID = PackageBases.MaintainerUID ' + - 'OR Users.ID = PackageComaintainers.UsersID ' + - 'WHERE PackageRequests.ID = ? AND ' + - 'Users.Suspended = 0', [reqid]) + 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') - self._cc = [row[0] for row in cur.fetchall()] - cur = conn.execute('SELECT Comments FROM PackageRequests WHERE ID = ?', - [reqid]) - self._text = cur.fetchone()[0] + + 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 @@ -485,29 +614,42 @@ class RequestOpenNotification(Notification): class RequestCloseNotification(Notification): - def __init__(self, conn, uid, reqid, reason): - self._user = username_from_id(conn, uid) if int(uid) else None - cur = conn.execute('SELECT DISTINCT Users.Email FROM PackageRequests ' + - 'INNER JOIN PackageBases ' + - 'ON PackageBases.ID = PackageRequests.PackageBaseID ' + - 'LEFT JOIN PackageComaintainers ' + - 'ON PackageComaintainers.PackageBaseID = PackageRequests.PackageBaseID ' + - 'INNER JOIN Users ' + - 'ON Users.ID = PackageRequests.UsersID ' + - 'OR Users.ID = PackageBases.MaintainerUID ' + - 'OR Users.ID = PackageComaintainers.UsersID ' + - 'WHERE PackageRequests.ID = ? AND ' + - 'Users.Suspended = 0', [reqid]) + + 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') - self._cc = [row[0] for row in cur.fetchall()] - cur = conn.execute('SELECT PackageRequests.ClosureComment, ' + - 'RequestTypes.Name, ' + - 'PackageRequests.PackageBaseName ' + - 'FROM PackageRequests ' + - 'INNER JOIN RequestTypes ' + - 'ON RequestTypes.ID = PackageRequests.ReqTypeID ' + - 'WHERE PackageRequests.ID = ?', [reqid]) - self._text, self._reqtype, self._pkgbase = cur.fetchone() + + 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 @@ -550,33 +692,41 @@ class RequestCloseNotification(Notification): class TUVoteReminderNotification(Notification): - def __init__(self, conn, vote_id): + def __init__(self, vote_id): self._vote_id = int(vote_id) - cur = conn.execute('SELECT Email, LangPreference FROM Users ' + - 'WHERE AccountTypeID IN (2, 4) AND ID NOT IN ' + - '(SELECT UserID FROM TU_Votes ' + - 'WHERE TU_Votes.VoteID = ?)', [vote_id]) - self._recipients = cur.fetchall() + + 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 + ) + 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 self._l10n.translate('TU Vote Reminder: Proposal {id}', - lang).format(id=self._vote_id) + return aurweb.l10n.translator.translate( + 'TU Vote Reminder: Proposal {id}', + lang).format(id=self._vote_id) def get_body(self, lang): - return self._l10n.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) + 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) def get_refs(self): return (aur_location + '/tu/?id=' + str(self._vote_id),) def main(): + db.get_engine() action = sys.argv[1] action_map = { 'send-resetkey': ResetKeyNotification, @@ -594,14 +744,10 @@ def main(): 'tu-vote-reminder': TUVoteReminderNotification, } - conn = aurweb.db.Connection() - - notification = action_map[action](conn, *sys.argv[2:]) + with db.begin(): + notification = action_map[action](*sys.argv[2:]) notification.send() - conn.commit() - conn.close() - if __name__ == '__main__': main() diff --git a/aurweb/scripts/pkgmaint.py b/aurweb/scripts/pkgmaint.py index 36da126f..2a2c638a 100755 --- a/aurweb/scripts/pkgmaint.py +++ b/aurweb/scripts/pkgmaint.py @@ -1,19 +1,25 @@ #!/usr/bin/env python3 -import time +from sqlalchemy import and_ -import aurweb.db +from aurweb import db, time +from aurweb.models import PackageBase + + +def _main(): + # One day behind. + limit_to = time.utcnow() - 86400 + + query = db.query(PackageBase).filter( + and_(PackageBase.SubmittedTS < limit_to, + PackageBase.PackagerUID.is_(None))) + db.delete_all(query) def main(): - conn = aurweb.db.Connection() - - limit_to = int(time.time()) - 86400 - conn.execute("DELETE FROM PackageBases WHERE " + - "SubmittedTS < ? AND PackagerUID IS NULL", [limit_to]) - - conn.commit() - conn.close() + db.get_engine() + with db.begin(): + _main() if __name__ == '__main__': diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index b64deedb..a2a796fd 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -1,25 +1,70 @@ #!/usr/bin/env python3 -import time +from typing import List -import aurweb.db +from sqlalchemy import and_, func +from sqlalchemy.sql.functions import coalesce +from sqlalchemy.sql.functions import sum as _sum + +from aurweb import db, time +from aurweb.models import PackageBase, PackageVote + + +def run_variable(pkgbases: List[PackageBase] = []) -> None: + """ + Update popularity on a list of PackageBases. + + If no PackageBase is included, we update the popularity + of every PackageBase in the database. + + :param pkgbases: List of PackageBase instances + """ + now = time.utcnow() + + # NumVotes subquery. + 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)) + ) + + with db.begin(): + query = db.query(PackageBase) + + ids = set() + if pkgbases: + ids = {pkgbase.ID for pkgbase in pkgbases} + query = query.filter(PackageBase.ID.in_(ids)) + + query.update({ + "NumVotes": votes_subq.scalar_subquery(), + "Popularity": pop_subq.scalar_subquery() + }) + + +def run_single(pkgbase: PackageBase) -> None: + """ 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. + + :param pkgbase: Instance of db.PackageBase + """ + run_variable([pkgbase]) + db.refresh(pkgbase) def main(): - conn = aurweb.db.Connection() - - conn.execute("UPDATE PackageBases SET NumVotes = (" + - "SELECT COUNT(*) FROM PackageVotes " + - "WHERE PackageVotes.PackageBaseID = PackageBases.ID)") - - now = int(time.time()) - conn.execute("UPDATE PackageBases SET Popularity = (" + - "SELECT COALESCE(SUM(POWER(0.98, (? - VoteTS) / 86400)), 0.0) " + - "FROM PackageVotes WHERE PackageVotes.PackageBaseID = " + - "PackageBases.ID AND NOT VoteTS IS NULL)", [now]) - - conn.commit() - conn.close() + db.get_engine() + run_variable() if __name__ == '__main__': diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 76865d27..2af5384e 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -1,16 +1,19 @@ #!/usr/bin/env python3 -import re -import pygit2 import sys + +from xml.etree.ElementTree import Element + import bleach import markdown +import pygit2 import aurweb.config -import aurweb.db -repo_path = aurweb.config.get('serve', 'repo-path') -commit_uri = aurweb.config.get('options', 'commit_uri') +from aurweb import db, logging, util +from aurweb.models import PackageComment + +logger = logging.get_logger(__name__) class LinkifyExtension(markdown.extensions.Extension): @@ -24,7 +27,7 @@ class LinkifyExtension(markdown.extensions.Extension): _urlre = (r'(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' r'(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))') - def extendMarkdown(self, md, md_globals): + 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) @@ -39,15 +42,15 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): """ def handleMatch(self, m, data): - el = markdown.util.etree.Element('a') + 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, md_globals): - processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b',md) + def extendMarkdown(self, md): + processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b', md) md.inlinePatterns.register(processor, 'flyspray-links', 118) @@ -60,9 +63,9 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): considered. """ - _repo = pygit2.Repository(repo_path) - def __init__(self, md, head): + 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) @@ -70,18 +73,14 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): oid = m.group(1) if oid not in self._repo: # Unkwown OID; preserve the orginal text. - return None, None, None + return (None, None, None) - prefixlen = 12 - while prefixlen < 40: - if oid[:prefixlen] in self._repo: - break - prefixlen += 1 - - el = markdown.util.etree.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 % (self._head, 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): @@ -91,9 +90,12 @@ class GitCommitsExtension(markdown.extensions.Extension): self._head = head super(markdown.extensions.Extension, self).__init__() - def extendMarkdown(self, md, md_globals): - processor = GitCommitsInlineProcessor(md, self._head) - md.inlinePatterns.register(processor, 'git-commits', 117) + def extendMarkdown(self, md): + try: + processor = GitCommitsInlineProcessor(md, self._head) + md.inlinePatterns.register(processor, 'git-commits', 117) + except pygit2.GitError: + logger.error(f"No git repository found for '{self._head}'.") class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): @@ -106,42 +108,46 @@ class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): class HeadingExtension(markdown.extensions.Extension): - def extendMarkdown(self, md, md_globals): + def extendMarkdown(self, md): # Priority doesn't matter since we don't conflict with other processors. md.treeprocessors.register(HeadingTreeprocessor(md), 'heading', 30) -def get_comment(conn, commentid): - cur = conn.execute('SELECT PackageComments.Comments, PackageBases.Name ' - 'FROM PackageComments INNER JOIN PackageBases ' - 'ON PackageBases.ID = PackageComments.PackageBaseID ' - 'WHERE PackageComments.ID = ?', [commentid]) - return cur.fetchone() +def save_rendered_comment(comment: PackageComment, html: str): + with db.begin(): + comment.RenderedComment = html -def save_rendered_comment(conn, commentid, html): - conn.execute('UPDATE PackageComments SET RenderedComment = ? WHERE ID = ?', - [html, commentid]) +def update_comment_render_fastapi(comment: PackageComment) -> None: + update_comment_render(comment) + + +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() + ]) + + allowed_tags = (bleach.sanitizer.ALLOWED_TAGS + + ['p', 'pre', 'h4', 'h5', 'h6', 'br', 'hr']) + html = bleach.clean(html, tags=allowed_tags) + save_rendered_comment(comment, html) + db.refresh(comment) def main(): - commentid = int(sys.argv[1]) - - conn = aurweb.db.Connection() - - text, pkgbase = get_comment(conn, commentid) - html = markdown.markdown(text, extensions=['fenced_code', - LinkifyExtension(), - FlysprayLinksExtension(), - GitCommitsExtension(pkgbase), - HeadingExtension()]) - allowed_tags = (bleach.sanitizer.ALLOWED_TAGS + - ['p', 'pre', 'h4', 'h5', 'h6', 'br', 'hr']) - html = bleach.clean(html, tags=allowed_tags) - save_rendered_comment(conn, commentid, html) - - conn.commit() - conn.close() + db.get_engine() + comment_id = int(sys.argv[1]) + comment = db.query(PackageComment).filter( + PackageComment.ID == comment_id + ).first() + update_comment_render(comment) if __name__ == '__main__': diff --git a/aurweb/scripts/tuvotereminder.py b/aurweb/scripts/tuvotereminder.py index eb3874e1..742fa6d4 100755 --- a/aurweb/scripts/tuvotereminder.py +++ b/aurweb/scripts/tuvotereminder.py @@ -1,27 +1,34 @@ #!/usr/bin/env python3 -import subprocess -import time +from sqlalchemy import and_ import aurweb.config -import aurweb.db + +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(): - conn = aurweb.db.Connection() + db.get_engine() - now = int(time.time()) - filter_from = now + 500 - filter_to = now + 172800 + now = time.utcnow() - cur = conn.execute("SELECT ID FROM TU_VoteInfo " + - "WHERE End >= ? AND End <= ?", - [filter_from, filter_to]) + start = aurweb.config.getint("tuvotereminder", "range_start") + filter_from = now + start - for vote_id in [row[0] for row in cur.fetchall()]: - subprocess.Popen((notify_cmd, 'tu-vote-reminder', str(vote_id))).wait() + 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__': diff --git a/aurweb/scripts/usermaint.py b/aurweb/scripts/usermaint.py index 1621d410..69f9db04 100755 --- a/aurweb/scripts/usermaint.py +++ b/aurweb/scripts/usermaint.py @@ -1,21 +1,29 @@ #!/usr/bin/env python3 -import time +from sqlalchemy import update -import aurweb.db +from aurweb import db, time +from aurweb.models import User + + +def _main(): + limit_to = time.utcnow() - 86400 * 7 + + 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) + db.get_session().execute(update_) def main(): - conn = aurweb.db.Connection() - - limit_to = int(time.time()) - 86400 * 7 - conn.execute("UPDATE Users SET LastLoginIPAddress = NULL " + - "WHERE LastLogin < ?", [limit_to]) - conn.execute("UPDATE Users SET LastSSHLoginIPAddress = NULL " + - "WHERE LastSSHLogin < ?", [limit_to]) - - conn.commit() - conn.close() + db.get_engine() + with db.begin(): + _main() if __name__ == '__main__': diff --git a/aurweb/spawn.py b/aurweb/spawn.py new file mode 100644 index 00000000..46f2f021 --- /dev/null +++ b/aurweb/spawn.py @@ -0,0 +1,297 @@ +""" +Provide an automatic way of spawing an HTTP test server running aurweb. + +It can be called from the command-line or from another Python module. + +This module uses a global state, since you can’t open two servers with the same +configuration anyway. +""" + + +import argparse +import atexit +import os +import os.path +import subprocess +import sys +import tempfile +import time + +from typing import Iterable, List + +import aurweb.config +import aurweb.schema + +from aurweb.exceptions import AurwebException + +children = [] +temporary_dir = None +verbosity = 0 +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)) + + +class ProcessExceptions(Exception): + """ + Compound exception used by stop() to list all the errors that happened when + terminating child processes. + """ + + def __init__(self, message, exceptions): + self.message = message + self.exceptions = exceptions + messages = [message] + [str(e) for e in exceptions] + 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}; + }} + }} + 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): + """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)) + + +def start(): + """ + Spawn the test server. If it is already running, do nothing. + + The server can be stopped with stop(), or is automatically stopped when the + Python process ends using atexit. + """ + if children: + return + atexit.register(stop) + + 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]) + + # FastAPI + fastapi_host, fastapi_port = aurweb.config.get( + "fastapi", "bind_address").rsplit(":", 1) + + # Logging config. + aurwebdir = aurweb.config.get("options", "aurwebdir") + fastapi_log_config = os.path.join(aurwebdir, "logging.conf") + + 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)] + } + backend_args = backend_args.get(asgi_backend) + 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""" + > Started nginx. + > + > PHP backend: http://{php_address} + > FastAPI backend: http://{fastapi_host}:{fastapi_port} + > + > PHP frontend: http://{php_host}:{PHP_NGINX_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]: + """ + Kill each process found in `children`. + + :param children: Iterable of child processes + :param exceptions: Exception memo + :return: `exceptions` + """ + for p in children: + try: + p.terminate() + if verbosity >= 1: + print(f":: Sent SIGTERM to {p.args}", file=sys.stderr) + except Exception as e: + exceptions.append(e) + return exceptions + + +def _wait_for_children(children: Iterable, exceptions: List[Exception] = []) \ + -> List[Exception]: + """ + Wait for each process to end found in `children`. + + :param children: Iterable of child processes + :param exceptions: Exception memo + :return: `exceptions` + """ + for p in children: + try: + rc = p.wait() + if rc != 0 and rc != -15: + # rc = -15 indicates the process was terminated with SIGTERM, + # which is to be expected since we called terminate on them. + raise Exception(f"Process {p.args} exited with {rc}") + except Exception as e: + exceptions.append(e) + return exceptions + + +def stop() -> None: + """ + Stop all the child processes. + + If an exception occurs during the process, the process continues anyway + because we don’t want to leave runaway processes around, and all the + exceptions are finally raised as a single ProcessExceptions. + + :raises: ProcessException + :return: None + """ + global children + atexit.unregister(stop) + exceptions = _kill_children(children) + exceptions = _wait_for_children(children, exceptions) + children = [] + if exceptions: + raise ProcessExceptions("Errors terminating the child processes:", + exceptions) + + +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") + 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 + with tempfile.TemporaryDirectory(prefix="aurweb-") as tmpdirname: + temporary_dir = tmpdirname + start() + try: + while True: + time.sleep(60) + except KeyboardInterrupt: + stop() diff --git a/aurweb/templates.py b/aurweb/templates.py new file mode 100644 index 00000000..d1830d91 --- /dev/null +++ b/aurweb/templates.py @@ -0,0 +1,140 @@ +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 + +# 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"]) + + +def register_filter(name: str) -> Callable: + """ A decorator that can be used to register a filter. + + Example + @register_filter("some_filter") + def some_filter(some_value: str) -> str: + return some_value.replace("-", "_") + + Jinja2 + {{ 'blah-blah' | some_filter }} + + :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. + """ + 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. """ + 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) + if commit_hash: + # Shorten commit_hash to a short Git hash. + commit_hash = commit_hash[:7] + + timezone = time.get_request_timezone(request) + return { + "request": request, + "commit_url": commit_url, + "commit_hash": commit_hash, + "language": l10n.get_request_language(request), + "languages": l10n.SUPPORTED_LANGUAGES, + "timezone": timezone, + "timezones": time.SUPPORTED_TIMEZONES, + "title": title, + "now": time.now(timezone), + "utcnow": time.utcnow(), + "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) + } + + +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). """ + context = make_context(request, title, next) + 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 + + return context + + +def base_template(path: str): + templates = copy.copy(_env) + return templates.get_template(path) + + +def render_raw_template(request: Request, path: str, context: dict): + """ 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 + # _environment being rendered without installing them into a global + # which is reused in this function. + templates = copy.copy(_env) + + translator = l10n.get_raw_translator_for_request(context.get("request")) + templates.install_gettext_translations(translator) + + template = templates.get_template(path) + return template.render(context) + + +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) diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py new file mode 100644 index 00000000..99671d69 --- /dev/null +++ b/aurweb/testing/__init__.py @@ -0,0 +1,68 @@ +import aurweb.db + +from aurweb import models + + +def setup_test_db(*args): + """ 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. + + The primary goal of this method is to configure empty tables + that tests can use from scratch. This means that tests using + this function should make sure they do not depend on external + records and keep their logic self-contained. + + Generally used inside of pytest fixtures, this function + can be used anywhere, but keep in mind its functionality when + doing so. + + Examples: + setup_test_db("Users", "Sessions") + + test_tables = ["Users", "Sessions"]; + setup_test_db(*test_tables) + """ + # Make sure that we've grabbed the engine before using the session. + aurweb.db.get_engine() + + tables = list(args) + if not tables: + tables = [ + models.AcceptedTerm.__tablename__, + models.ApiRateLimit.__tablename__, + models.Ban.__tablename__, + models.Group.__tablename__, + models.License.__tablename__, + models.OfficialProvider.__tablename__, + models.Package.__tablename__, + models.PackageBase.__tablename__, + models.PackageBlacklist.__tablename__, + models.PackageComaintainer.__tablename__, + models.PackageComment.__tablename__, + models.PackageDependency.__tablename__, + models.PackageGroup.__tablename__, + models.PackageKeyword.__tablename__, + models.PackageLicense.__tablename__, + models.PackageNotification.__tablename__, + models.PackageRelation.__tablename__, + models.PackageRequest.__tablename__, + models.PackageSource.__tablename__, + models.PackageVote.__tablename__, + models.Session.__tablename__, + models.SSHPubKey.__tablename__, + models.Term.__tablename__, + models.TUVote.__tablename__, + models.TUVoteInfo.__tablename__, + models.User.__tablename__, + ] + + aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 0") + for table in tables: + aurweb.db.get_session().execute(f"DELETE FROM {table}") + aurweb.db.get_session().execute("SET FOREIGN_KEY_CHECKS = 1") + aurweb.db.get_session().expunge_all() + + +def noop(*args, **kwargs) -> None: + return diff --git a/aurweb/testing/alpm.py b/aurweb/testing/alpm.py new file mode 100644 index 00000000..6015d859 --- /dev/null +++ b/aurweb/testing/alpm.py @@ -0,0 +1,87 @@ +import hashlib +import os +import re +import shutil +import subprocess + +from typing import List + +from aurweb import logging, util +from aurweb.templates import base_template + +logger = logging.get_logger(__name__) + + +class AlpmDatabase: + """ + Fake libalpm database management class. + + This class can be used to add or remove packages from a + test repository. + """ + repo = "test" + + def __init__(self, database_root: str): + self.root = database_root + self.local = os.path.join(self.root, "local") + self.remote = os.path.join(self.root, "remote") + self.repopath = os.path.join(self.remote, self.repo) + + # Make directories. + os.makedirs(self.local) + os.makedirs(self.remote) + + def _get_pkgdir(self, pkgname: str, pkgver: str, repo: str) -> str: + pkgfile = f"{pkgname}-{pkgver}-1" + pkgdir = os.path.join(self.remote, repo, pkgfile) + os.makedirs(pkgdir) + return pkgdir + + def add(self, pkgname: str, pkgver: str, arch: str, + provides: List[str] = []) -> None: + context = { + "pkgname": pkgname, + "pkgver": pkgver, + "arch": arch, + "provides": provides + } + template = base_template("testing/alpm_package.j2") + pkgdir = self._get_pkgdir(pkgname, pkgver, self.repo) + desc = os.path.join(pkgdir, "desc") + with open(desc, "w") as f: + f.write(template.render(context)) + + self.compile() + + def remove(self, pkgname: str): + files = os.listdir(self.repopath) + logger.info(f"Files: {files}") + expr = "^" + pkgname + r"-[0-9.]+-1$" + logger.info(f"Expression: {expr}") + to_delete = filter(lambda e: re.match(expr, e), files) + + for target in to_delete: + logger.info(f"Deleting {target}") + path = os.path.join(self.repopath, target) + shutil.rmtree(path) + + self.compile() + + def clean(self) -> None: + db_file = os.path.join(self.remote, "test.db") + try: + os.remove(db_file) + except Exception: + pass + + def compile(self) -> None: + 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}" + + # Print out the md5 hash value of the new test.db. + test_db = os.path.join(self.remote, "test.db") + db_hash = util.file_hash(test_db, hashlib.md5) + logger.debug(f"{test_db}: {db_hash}") diff --git a/aurweb/testing/email.py b/aurweb/testing/email.py new file mode 100644 index 00000000..c0be2797 --- /dev/null +++ b/aurweb/testing/email.py @@ -0,0 +1,155 @@ +import base64 +import binascii +import copy +import email +import os +import re +import sys + +from typing import TextIO + + +class Email: + """ + An email class used for testing. + + This class targets a specific serial of emails for PYTEST_CURRENT_TEST. + As emails are sent out with util/sendmail, the serial number increases, + starting at 1. + + Email content sent out by aurweb is always base64-encoded. Email.parse() + decodes that for us and puts it into Email.body. + + Example: + + # Get the {test_suite}_{test_function}.1.txt email. + email = Email(1).parse() + print(email.body) + print(email.headers) + + """ + TEST_DIR = "test-emails" + + def __init__(self, serial: int = 1, autoparse: bool = True): + self.serial = serial + self.content = self._get() + + if autoparse: + self._parse() + + @staticmethod + def email_prefix(suite: bool = False) -> str: + """ + Get the email prefix. + + We find the email prefix by reducing PYTEST_CURRENT_TEST to + either {test_suite}_{test_function}. If `suite` is set, we + reduce it to {test_suite} only. + + :param suite: Reduce PYTEST_CURRENT_TEST to {test_suite} + :return: Email prefix with '/', '.', ',', and ':' chars replaced by '_' + """ + value = os.environ.get("PYTEST_CURRENT_TEST", "email").split(" ")[0] + if suite: + value = value.split(":")[0] + return re.sub(r'(\/|\.|,|:)', "_", value) + + @staticmethod + def count() -> int: + """ + Count the current number of emails sent from the test. + + This function is **only** supported inside of pytest functions. + Do not use it elsewhere as data races will occur. + + :return: Number of emails sent by the current test + """ + files = os.listdir(Email.TEST_DIR) + prefix = Email.email_prefix() + expr = "^" + prefix + r"\.\d+\.txt$" + subset = filter(lambda e: re.match(expr, e), files) + return len(list(subset)) + + def _email_path(self) -> str: + filename = self.email_prefix() + f".{self.serial}.txt" + return os.path.join(Email.TEST_DIR, filename) + + def _get(self) -> str: + """ + Get this email's content by reading its file. + + :return: Email content + """ + path = self._email_path() + with open(path) as f: + return f.read() + + def _parse(self) -> "Email": + """ + Parse this email and base64-decode the body. + + This function populates Email.message, Email.headers and Email.body. + + Additionally, after parsing, we write over our email file with + self.glue()'d content (base64-decoded). This is done for ease + of inspection by users. + + :return: self + """ + self.message = email.message_from_string(self.content) + self.headers = dict(self.message) + + # aurweb email notifications always have base64 encoded content. + # Decode it here so self.body is human readable. + try: + self.body = base64.b64decode(self.message.get_payload()).decode() + except (binascii.Error, UnicodeDecodeError): + self.body = self.message.get_payload() + + path = self._email_path() + with open(path, "w") as f: + f.write(self.glue()) + + return self + + def parse(self) -> "Email": + return self + + def glue(self) -> str: + """ + Glue parsed content back into a complete email document, but + base64-decoded this time. + + :return: Email document as a string + """ + headers = copy.copy(self.headers) + + if "Content-Transfer-Encoding" in headers: + headers.pop("Content-Transfer-Encoding") + + output = [] + for k, v in headers.items(): + output.append(f"{k}: {v}") + output.append("") + output.append(self.body) + return "\n".join(output) + + @staticmethod + def dump(file: TextIO = sys.stdout) -> None: + """ + Dump emails content to `file`. + + This function is intended to be used to debug email issues + while testing something relevent to email. + + :param file: Writable file object + """ + lines = [] + for i in range(Email.count()): + email = Email(i + 1) + lines += [ + f"== Email #{i + 1} ==", + email.glue(), + f"== End of Email #{i + 1}" + ] + print("\n".join(lines), file=file) diff --git a/aurweb/testing/filelock.py b/aurweb/testing/filelock.py new file mode 100644 index 00000000..3a18c153 --- /dev/null +++ b/aurweb/testing/filelock.py @@ -0,0 +1,32 @@ +import hashlib +import os + +from typing import Callable + +from posix_ipc import O_CREAT, Semaphore + +from aurweb import logging + +logger = logging.get_logger(__name__) + + +def default_on_create(path): + logger.info(f"Filelock at {path} acquired.") + + +class FileLock: + def __init__(self, tmpdir, name: str): + self.root = tmpdir + self.path = str(self.root / name) + self._file = str(self.root / (f"{name}.1")) + + def lock(self, on_create: Callable = default_on_create): + hash = hashlib.sha1(self.path.encode()).hexdigest() + with Semaphore(f"/{hash}-lock", flags=O_CREAT, initial_value=1): + retval = os.path.exists(self._file) + if not retval: + with open(self._file, "w") as f: + f.write("1") + on_create(self.path) + + return retval diff --git a/aurweb/testing/git.py b/aurweb/testing/git.py new file mode 100644 index 00000000..019d870f --- /dev/null +++ b/aurweb/testing/git.py @@ -0,0 +1,110 @@ +import os +import shlex + +from subprocess import PIPE, Popen +from typing import Tuple + +import py + +from aurweb.models import Package +from aurweb.templates import base_template +from aurweb.testing.filelock import FileLock + + +class GitRepository: + """ + A Git repository class to be used for testing. + + Expects a `tmpdir` fixture on construction, which an 'aur.git' + git repository will be created in. After this class is constructed, + users can call GitRepository.exec for git repository operations. + """ + + def __init__(self, tmpdir: py.path.local): + self.file_lock = FileLock(tmpdir, "aur.git") + 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()) + + def _exec_repository(self, cmdline: str) -> Tuple[int, str, str]: + return self._exec(cmdline, cwd=str(self.file_lock.path)) + + def exec(self, cmdline: str) -> Tuple[int, str, str]: + return self._exec_repository(cmdline) + + def _setup(self, path: str) -> None: + """ + Setup the git repository from scratch. + + Create the `path` directory and run the INSTALL recommended + git initialization commands inside of it. Additionally, install + aurweb.git.update to {path}/hooks/update. + + :param path: Repository path not yet created + """ + + os.makedirs(path) + + commands = [ + "git init -q", + "git config --local transfer.hideRefs '^refs/'", + "git config --local --add transfer.hideRefs '!refs/'", + "git config --local --add transfer.hideRefs '!HEAD'", + "git config --local commit.gpgsign false", + "git config --local user.name 'Test User'", + "git config --local user.email 'test@example.org'", + ] + for cmdline in commands: + return_code, out, err = self.exec(cmdline) + assert return_code == 0 + + # This is also done in the INSTALL script to give the `aur` + # ssh user permissions on the repository. We don't need it + # during testing, since our testing user will be controlling + # the repository. It is left here as a note. + # self.exec("chown -R aur .") + + def commit(self, pkg: Package, message: str): + """ + Commit a Package record to the git repository. + + This function generates a PKGBUILD and .SRCINFO based on + `pkg`, then commits them to the repository with the + `message` commit message. + + :param pkg: Package instance + :param message: Commit message + :return: Output of `git rev-parse HEAD` after committing + """ + ref = f"refs/namespaces/{pkg.Name}/refs/heads/master" + rc, out, err = self.exec(f"git checkout -q --orphan {ref}") + assert rc == 0, f"{(rc, out, err)}" + + # Path to aur.git repository. + repo = os.path.join(self.file_lock.path) + + licenses = [f"'{p.License.Name}'" for p in pkg.package_licenses] + depends = [f"'{p.DepName}'" for p in pkg.package_dependencies] + pkgbuild = base_template("testing/PKGBUILD.j2") + pkgbuild_path = os.path.join(repo, "PKGBUILD") + with open(pkgbuild_path, "w") as f: + data = pkgbuild.render(pkg=pkg, licenses=licenses, depends=depends) + f.write(data) + + srcinfo = base_template("testing/SRCINFO.j2") + srcinfo_path = os.path.join(repo, ".SRCINFO") + with open(srcinfo_path, "w") as f: + f.write(srcinfo.render(pkg=pkg)) + + rc, out, err = self.exec("git add PKGBUILD .SRCINFO") + assert rc == 0, f"{(rc, out, err)}" + + rc, out, err = self.exec(f"git commit -q -m '{message}'") + assert rc == 0, f"{(rc, out, err)}" + + # Return stdout of `git rev-parse HEAD`, which is the new commit hash. + return self.exec("git rev-parse HEAD")[1] diff --git a/aurweb/testing/html.py b/aurweb/testing/html.py new file mode 100644 index 00000000..f01aaf3d --- /dev/null +++ b/aurweb/testing/html.py @@ -0,0 +1,25 @@ +from io import StringIO +from typing import List + +from lxml import etree + +parser = etree.HTMLParser() + + +def parse_root(html: str) -> etree.Element: + """ Parse an lxml.etree.ElementTree root from html content. + + :param html: HTML markup + :return: etree.Element + """ + return etree.parse(StringIO(html), parser) + + +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]: + root = parse_root(content) + return root.xpath('//ul[@class="success"]/li') diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py new file mode 100644 index 00000000..be13ab77 --- /dev/null +++ b/aurweb/testing/requests.py @@ -0,0 +1,45 @@ +from typing import Dict + +import aurweb.config + + +class User: + """ A fake User model. """ + # Fake columns. + LangPreference = aurweb.config.get("options", "default_lang") + Timezone = aurweb.config.get("options", "default_timezone") + + # A fake authenticated flag. + authenticated = False + + def is_authenticated(self): + return self.authenticated + + +class Client: + """ A fake FastAPI Request.client object. """ + # A fake host. + host = "127.0.0.1" + + +class URL: + path = "/" + + +class Request: + """ 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": + self.user = user + self.user.authenticated = authenticated + + self.method = method.upper() + self.headers = headers + self.cookies = cookies diff --git a/aurweb/testing/smtp.py b/aurweb/testing/smtp.py new file mode 100644 index 00000000..da64c93f --- /dev/null +++ b/aurweb/testing/smtp.py @@ -0,0 +1,42 @@ +""" Fake SMTP clients that can be used for testing. """ + + +class FakeSMTP: + """ A fake version of smtplib.SMTP used for testing. """ + + starttls_enabled = False + use_ssl = False + + def __init__(self): + self.emails = [] + self.count = 0 + self.ehlo_count = 0 + self.quit_count = 0 + self.set_debuglevel_count = 0 + self.user = None + self.passwd = None + + def ehlo(self) -> None: + self.ehlo_count += 1 + + def starttls(self) -> None: + self.starttls_enabled = True + + def set_debuglevel(self, level: int = 0) -> None: + self.set_debuglevel_count += 1 + + def login(self, user: str, passwd: str) -> None: + self.user = user + self.passwd = passwd + + def sendmail(self, sender: str, to: str, msg: bytes) -> None: + self.emails.append((sender, to, msg.decode())) + self.count += 1 + + def quit(self) -> None: + self.quit_count += 1 + + +class FakeSMTP_SSL(FakeSMTP): + """ A fake version of smtplib.SMTP_SSL used for testing. """ + use_ssl = True diff --git a/aurweb/time.py b/aurweb/time.py new file mode 100644 index 00000000..a97ca986 --- /dev/null +++ b/aurweb/time.py @@ -0,0 +1,84 @@ +import zoneinfo + +from collections import OrderedDict +from datetime import datetime +from urllib.parse import unquote +from zoneinfo import ZoneInfo + +from fastapi import Request + +import aurweb.config + + +def tz_offset(name: str): + """ Get a timezone offset in the form "+00:00" by its name. + + Example: tz_offset('America/Los_Angeles') + + :param name: Timezone name + :return: UTC offset in the form "+00:00" + """ + dt = datetime.now(tz=zoneinfo.ZoneInfo(name)) + + # Our offset in hours. + offset = dt.utcoffset().total_seconds() / 60 / 60 + + # Prefix the offset string with a - or +. + offset_string = '-' if offset < 0 else '+' + + # Remove any negativity from the offset. We want a good offset. :) + offset = abs(offset) + + # Truncate the floating point digits, giving the hours. + hours = int(offset) + + # Subtract hours from the offset, and multiply the remaining fraction + # (0 - 0.99[repeated]) with 60 minutes to get the number of minutes + # remaining in the hour. + minutes = int((offset - hours) * 60) + + # Pad the hours and minutes by two places. + offset_string += "{:0>2}:{:0>2}".format(hours, minutes) + 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])) +}) + + +def get_request_timezone(request: Request): + """ Get a request's timezone by its AURTZ cookie. 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)) + + +def now(timezone: str) -> datetime: + """ + Get the current timezone-localized timestamp. + + :param timezone: Valid timezone supported by ZoneInfo + :return: Current localized datetime + """ + return datetime.now(tz=ZoneInfo(timezone)) + + +def utcnow() -> int: + """ + Get the current UTC timestamp. + + :return: Current UTC timestamp + """ + return int(datetime.utcnow().timestamp()) diff --git a/aurweb/users/__init__.py b/aurweb/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/aurweb/users/update.py b/aurweb/users/update.py new file mode 100644 index 00000000..685dfd80 --- /dev/null +++ b/aurweb/users/update.py @@ -0,0 +1,109 @@ +from typing import Any, Dict + +from fastapi import Request + +from aurweb import cookies, db, models, time +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: + now = time.utcnow() + with db.begin(): + user.Username = U or user.Username + user.Email = E or user.Email + user.HideEmail = strtobool(H) + user.BackupEmail = BE or user.BackupEmail + user.RealName = R or user.RealName + user.Homepage = HP or user.Homepage + user.IRCNick = I or user.IRCNick + user.PGPKey = K or user.PGPKey + user.Suspended = strtobool(S) + user.InactivityTS = now * int(strtobool(J)) + user.CommentNotify = strtobool(CN) + user.UpdateNotify = strtobool(UN) + user.OwnershipNotify = strtobool(ON) + + +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: + if TZ and TZ != user.Timezone: + with db.begin(): + user.Timezone = TZ + context["language"] = TZ + + +def ssh_pubkey(PK: str = str(), + user: models.User = None, + **kwargs) -> None: + # If a PK is given, compare it against the target user's PK. + if PK: + # Get the second token in the public key, which is the actual key. + pubkey = PK.strip().rstrip() + parts = pubkey.split(" ") + if len(parts) == 3: + # Remove the host part. + pubkey = parts[0] + " " + parts[1] + fingerprint = get_fingerprint(pubkey) + if not user.ssh_pub_key: + # No public key exists, create one. + with db.begin(): + db.create(models.SSHPubKey, UserID=user.ID, + PubKey=pubkey, Fingerprint=fingerprint) + elif user.ssh_pub_key.PubKey != pubkey: + # A public key already exists, update it. + with db.begin(): + user.ssh_pub_key.PubKey = pubkey + user.ssh_pub_key.Fingerprint = fingerprint + elif user.ssh_pub_key: + # Else, if the user has a public key already, delete it. + with db.begin(): + db.delete(user.ssh_pub_key) + + +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: + if P and not user.valid_password(P): + # Remove the fields we consumed for passwords. + context["P"] = context["C"] = str() + + # If a password was given and it doesn't match the user's, update it. + with db.begin(): + 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)) diff --git a/aurweb/users/util.py b/aurweb/users/util.py new file mode 100644 index 00000000..e9635f08 --- /dev/null +++ b/aurweb/users/util.py @@ -0,0 +1,19 @@ +from http import HTTPStatus + +from fastapi import HTTPException + +from aurweb import db +from aurweb.models import User + + +def get_user_by_name(username: str) -> User: + """ + Query a user by its username. + + :param username: User.Username + :return: User instance + """ + user = db.query(User).filter(User.Username == username).first() + if not user: + raise HTTPException(status_code=int(HTTPStatus.NOT_FOUND)) + return db.refresh(user) diff --git a/aurweb/users/validate.py b/aurweb/users/validate.py new file mode 100644 index 00000000..bbd6082a --- /dev/null +++ b/aurweb/users/validate.py @@ -0,0 +1,200 @@ +""" +Validation functions for account registration and edit fields. +Each of these functions extracts a subset of keyword arguments +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.auth import creds +from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token +from aurweb.exceptions import ValidationError +from aurweb.models.account_type import ACCOUNT_TYPE_NAME +from aurweb.models.ssh_pub_key import get_fingerprint + +logger = logging.get_logger(__name__) + + +def invalid_fields(E: str = str(), U: str = str(), **kwargs) -> None: + if not E or not U: + raise ValidationError(["Missing a required field."]) + + +def invalid_suspend_permission(request: Request = None, + user: models.User = None, + J: bool = False, + **kwargs) -> None: + if not request.user.is_elevated() and J != bool(user.InactivityTS): + raise ValidationError([ + "You do not have permission to suspend accounts."]) + + +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.", + [ + _("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: + 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) + ]) + elif not C: + raise ValidationError(["Please confirm your new password."]) + elif P != C: + raise ValidationError(["Password fields do not match."]) + + +def is_banned(request: Request = None, **kwargs) -> None: + host = request.client.host + 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." + ]) + + +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."]) + + +def invalid_email(E: str = str(), **kwargs) -> None: + if not util.valid_email(E): + raise ValidationError(["The email address is invalid."]) + + +def invalid_backup_email(BE: str = str(), **kwargs) -> None: + if BE and not util.valid_email(BE): + raise ValidationError(["The backup email address is invalid."]) + + +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."]) + + +def invalid_pgp_key(K: str = str(), **kwargs) -> None: + if K and not util.valid_pgp_fingerprint(K): + raise ValidationError(["The PGP key fingerprint is invalid."]) + + +def invalid_ssh_pubkey(PK: str = str(), user: models.User = None, + _: l10n.Translator = None, **kwargs) -> None: + if PK: + invalid_exc = ValidationError(["The SSH public key is invalid."]) + if not util.valid_ssh_pubkey(PK): + raise invalid_exc + + fingerprint = get_fingerprint(PK.strip().rstrip()) + if not fingerprint: + raise invalid_exc + + 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, "") + ]) + + +def invalid_language(L: str = str(), **kwargs) -> None: + if L and L not in l10n.SUPPORTED_LANGUAGES: + raise ValidationError(["Language is not currently supported."]) + + +def invalid_timezone(TZ: str = str(), **kwargs) -> None: + if TZ and TZ not in time.SUPPORTED_TIMEZONES: + 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() + if db.query(exists).scalar(): + # If the username already exists... + 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() + if db.query(exists).scalar(): + # If the email already exists... + 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: + 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."]) + 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 + raise ValidationError([error]) + + logger.debug(f"Trusted User '{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: + if captcha_salt and captcha_salt not in get_captcha_salts(): + raise ValidationError(["This CAPTCHA has expired. Please try again."]) + + if captcha: + answer = get_captcha_answer(get_captcha_token(captcha_salt)) + if captcha != answer: + raise ValidationError(["The entered CAPTCHA answer is invalid."]) diff --git a/aurweb/util.py b/aurweb/util.py new file mode 100644 index 00000000..776cf516 --- /dev/null +++ b/aurweb/util.py @@ -0,0 +1,196 @@ +import base64 +import math +import re +import secrets +import string + +from datetime import datetime +from distutils.util import strtobool as _strtobool +from http import HTTPStatus +from typing import Callable, Iterable, Tuple +from urllib.parse import urlparse + +import fastapi +import pygit2 + +from email_validator import EmailNotValidError, EmailUndeliverableError, validate_email +from fastapi.responses import JSONResponse + +import aurweb.config + +from aurweb import defaults, logging + +logger = 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)]) + + +def make_nonce(length: int = 8): + """ 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). """ + return secrets.token_hex(math.ceil(length / 2))[:length] + + +def valid_username(username): + min_len = aurweb.config.getint("options", "username_min_len") + max_len = aurweb.config.getint("options", "username_max_len") + if not (min_len <= len(username) <= max_len): + return False + + # 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) + + +def valid_email(email): + try: + validate_email(email) + except EmailUndeliverableError: + return False + except EmailNotValidError: + return False + return True + + +def valid_homepage(homepage): + try: + parts = urlparse(homepage) + except ValueError: + return False + return parts.scheme in ("http", "https") and bool(parts.netloc) + + +def valid_password(password): + min_len = aurweb.config.getint("options", "passwd_min_len") + return len(password) >= min_len + + +def valid_pgp_fingerprint(fp): + fp = fp.replace(" ", "") + try: + # Attempt to convert the fingerprint to an int via base16. + # If it can't, it's not a hex string. + int(fp, 16) + except ValueError: + return False + + # Check the length; must be 40 hexadecimal digits. + return len(fp) == 40 + + +def valid_ssh_pubkey(pk): + valid_prefixes = aurweb.config.get("auth", "valid-keytypes") + valid_prefixes = set(valid_prefixes.split(" ")) + + has_valid_prefix = False + for prefix in valid_prefixes: + if "%s " % prefix in pk: + has_valid_prefix = True + break + if not has_valid_prefix: + return False + + tokens = pk.strip().rstrip().split(" ") + if len(tokens) < 2: + return False + + return base64.b64encode(base64.b64decode(tokens[1])).decode() == tokens[1] + + +def jsonify(obj): + """ Perform a conversion on obj if it's needed. """ + if isinstance(obj, datetime): + obj = int(obj.timestamp()) + return obj + + +def get_ssh_fingerprints(): + return aurweb.config.get_section("fingerprints") or {} + + +def apply_all(iterable: Iterable, fn: Callable): + for item in iterable: + fn(item) + return iterable + + +def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: + try: + offset = int(offset) + except ValueError: + offset = defaults.O + + try: + per_page = int(per_page) + except ValueError: + per_page = defaults.PP + + return (offset, per_page) + + +def strtobool(value: str) -> bool: + if isinstance(value, str): + return _strtobool(value) + return value + + +def file_hash(filepath: str, hash_function: Callable) -> str: + """ + Return a hash of filepath contents using `hash_function`. + + `hash_function` can be any one of the hashlib module's hash + functions which implement the `hexdigest()` method -- e.g. + hashlib.sha1, hashlib.md5, etc. + + :param filepath: Path to file you want to hash + :param hash_function: hashlib hash function + :return: hash_function(filepath_content).hexdigest() + """ + with open(filepath, "rb") as f: + hash_ = hash_function(f.read()) + return hash_.hexdigest() + + +def git_search(repo: pygit2.Repository, commit_hash: str) -> int: + """ + Return the shortest prefix length matching `commit_hash` found. + + :param repo: pygit2.Repository instance + :param commit_hash: Full length commit hash + :return: Shortest unique prefix length found + """ + prefixlen = 12 + while prefixlen < len(commit_hash): + if commit_hash[:prefixlen] in repo: + break + prefixlen += 1 + return prefixlen + + +async def error_or_result(next: Callable, *args, **kwargs) \ + -> fastapi.Response: + """ + Try to return a response from `next`. + + If RuntimeError is raised during next(...) execution, return a + 500 with the exception's error as a JSONResponse. + + :param next: Callable of the next fastapi route callback + :param *args: Variable number of arguments passed to the endpoint + :param **kwargs: Optional kwargs to pass to the endpoint + :return: next(...) retval; if an exc is raised: a 500 response + """ + try: + response = await next(*args, **kwargs) + except RuntimeError as exc: + logger.error(f"RuntimeError: {exc}") + status_code = HTTPStatus.INTERNAL_SERVER_ERROR + return JSONResponse({"error": str(exc)}, status_code=status_code) + return response diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..6ee495a3 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,76 @@ +# configuration file for git-cliff (0.1.0) + +[changelog] +# changelog header +header = """ +# Changelog +All notable feature additions, bug fixes and changes to this project will be \ +documented in this file.\n +""" +# template for the changelog body +# https://tera.netlify.app/docs/#introduction +body = """ +{% if version %}\ + #### Release [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + {% set head = commits | last %}\ + | Branch | HEAD | Status | Coverage | + |--------|------|--------|----------| + | [pu](https://gitlab.archlinux.org/archlinux/aurweb/-/tree/pu) | [{{ head.id | truncate(length=8, end="") }}](https://gitlab.archlinux.org/archlinux/aurweb/-/commits/{{ head.id }}) | ![pipeline](https://gitlab.archlinux.org/archlinux/aurweb/badges/pu/pipeline.svg?key_text=build) | ![coverage](https://gitlab.archlinux.org/archlinux/aurweb/badges/pu/coverage.svg) | +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | lower }} + | Commit | Message | + |--------|---------| \ + {% for commit in commits %} + | [{{ commit.id | truncate(length=8, end="") }}](https://gitlab.archlinux.org/archlinux/aurweb/-/commit/{{ commit.id }}) | {{ commit.message }} |\ + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespaces from the template +trim = true +# changelog footer +footer = """ +## Notes + +See a general project status overview at \ +https://gitlab.archlinux.org/archlinux/aurweb/-/wikis/Home. + +To contribute with testing of the modern aurweb FastAPI port development, visit \ +https://gitlab.archlinux.org/archlinux/aurweb/-/wikis/Testing-Guide. + +To file a bug, create an issue using the Bug template by following the link: \ +https://gitlab.archlinux.org/archlinux/aurweb/-/issues/new?issuable_template=Bug. + +To provide feedback, create an issue using the Feedback template by following +the link: \ +https://gitlab.archlinux.org/archlinux/aurweb/-/issues/new?issuable_template=Feedback. + + +""" + +[git] +# allow only conventional commits +# https://www.conventionalcommits.org +conventional_commits = true +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "Features"}, + { message = "^fix", group = "Bug Fixes"}, + { message = "^doc", group = "Documentation"}, + { message = "^perf", group = "Performance"}, + { message = "^change", group = "Changes" }, + { message = "^refactor", group = "Refactor"}, + { message = "^style", group = "Styling"}, + { message = "^test", group = "Testing"}, + { message = "^housekeep", group = "Housekeeping"}, + { message = "^chore\\(release\\): prepare for", skip = true}, + { message = "^chore", group = "Miscellaneous Tasks"}, + { body = ".*security", group = "Security"}, +] +# filter out the commits that are not matched by commit parsers +filter_commits = false +# glob pattern for matching git tags +tag_pattern = "*[0-9]*" +# regex for skipping tags +skip_tags = "v0.1.0-beta.1" diff --git a/conf/config.defaults b/conf/config.defaults index 50228591..cb96bad2 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -2,9 +2,10 @@ backend = mysql host = localhost socket = /var/run/mysqld/mysqld.sock +;port = 3306 name = AUR user = aur -password = aur +;password = aur [options] username_min_len = 3 @@ -21,6 +22,7 @@ aur_location = https://aur.archlinux.org git_clone_uri_anon = https://aur.archlinux.org/%s.git git_clone_uri_priv = ssh://aur@aur.archlinux.org/%s.git max_rpc_results = 5000 +max_search_results = 2500 max_depends = 1000 aur_request_ml = aur-requests@lists.archlinux.org request_idle_time = 1209600 @@ -33,19 +35,29 @@ commit_uri = /cgit/aur.git/commit/?h=%s&id=%s snapshot_uri = /cgit/aur.git/snapshot/%s.tar.gz enable-maintenance = 1 maintenance-exceptions = 127.0.0.1 -render-comment-cmd = /usr/local/bin/aurweb-rendercomment -localedir = /srv/http/aurweb/aur.git/web/locale/ -# memcache or apc +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 = 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 [ratelimit] request_limit = 4000 window_length = 86400 +; Force-utilize cache for ratelimiting. In FastAPI, forced cache (1) +; will cause the ratelimit path to use a real or fake Redis instance +; depending on the configured options.cache setting. Otherwise, +; cache will be ignored and the database will be used. +cache = 1 [notifications] -notify-cmd = /usr/local/bin/aurweb-notify +notify-cmd = /usr/bin/aurweb-notify sendmail = smtp-server = localhost smtp-port = 25 @@ -55,6 +67,9 @@ smtp-user = smtp-password = sender = notify@aur.archlinux.org reply-to = noreply@aur.archlinux.org +; Administration email which will receive notifications about +; various server details like uncaught exceptions. +postmaster = admin@example.org [fingerprints] Ed25519 = SHA256:HQ03dn6EasJHNDlt51KpQpFkT3yBX83x7BoIkA1iv2k @@ -66,14 +81,22 @@ RSA = SHA256:Ju+yWiMb/2O+gKQ9RJCDqvRg7l+Q95KFAeqM5sr6l2s ; https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.u2f valid-keytypes = ssh-rsa ssh-dss ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-ed25519 sk-ecdsa-sha2-nistp256@openssh.com sk-ecdsa-sha2-nistp256-cert-v01@openssh.com sk-ssh-ed25519@openssh.com sk-ssh-ed25519-cert-v01@openssh.com username-regex = [a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$ -git-serve-cmd = /usr/local/bin/aurweb-git-serve +git-serve-cmd = /usr/bin/aurweb-git-serve ssh-options = restrict +[sso] +openid_configuration = +client_id = +client_secret = + +[fastapi] +session_secret = + [serve] repo-path = /srv/http/aurweb/aur.git/ repo-regex = [a-z0-9][a-z0-9.+_-]*$ git-shell-cmd = /usr/bin/git-shell -git-update-cmd = /usr/local/bin/aurweb-git-update +git-update-cmd = /usr/bin/aurweb-git-update ssh-cmdline = ssh aur@aur.archlinux.org [update] @@ -82,11 +105,32 @@ max-blob-size = 256000 [aurblup] db-path = /srv/http/aurweb/aurblup/ sync-dbs = core extra community multilib testing community-testing -server = ftp://mirrors.kernel.org/archlinux/%s/os/x86_64 +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 + +[devel] +; commit_url is a format string used to produce a link to a commit hash. +commit_url = https://gitlab.archlinux.org/archlinux/aurweb/-/commits/%s + +; If commit_hash is configured, a link to the commit based on commit_url +; will be displayed in aurweb's footer with the release version. +; This allows us to diagnose which commit a particular instance is on +; during testing of development branches. +; Example deployment configuration step: +; 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 +; votes that they should make. +; Reminders will be sent out for all votes that a TU has not yet +; voted on based on `now + range_start <= End <= now + range_end`. +range_start = 500 +range_end = 172800 diff --git a/conf/config.dev b/conf/config.dev new file mode 100644 index 00000000..923c34ff --- /dev/null +++ b/conf/config.dev @@ -0,0 +1,80 @@ +; Configuration file for aurweb development. +; +; Options are implicitly inherited from conf/config.defaults, which lists all +; available options for productions, and their default values. This current file +; overrides only options useful for development, and introduces +; development-specific options too. + +[database] +; PHP options: mysql, sqlite. +; FastAPI options: mysql. +backend = mysql + +; If using sqlite, set name to the database file path. +name = aurweb + +; 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 = localhost +;port = 3306 +socket = /var/run/mysqld/mysqld.sock + +[options] +aurwebdir = YOUR_AUR_ROOT +aur_location = http://127.0.0.1:8080 +disable_http_login = 0 +enable-maintenance = 0 +localedir = YOUR_AUR_ROOT/web/locale +; In production, salt_rounds should be higher; suggested: 12. +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 +traceback = 1 + +[notifications] +; For development/testing, use /usr/bin/sendmail +sendmail = YOUR_AUR_ROOT/util/sendmail +sender = notify@localhost +reply-to = noreply@localhost + +; Single sign-on; see doc/sso.txt. +[sso] +openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/openid-configuration +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 + +; Passphrase FastAPI uses to sign client-side sessions. +session_secret = secret + +[devel] +;commit_hash = 1234567 + +[mkpkglists] +archivedir = /var/lib/aurweb/archives +packagesfile = /var/lib/aurweb/archives/packages.gz +packagesmetafile = /var/lib/aurweb/archives/packages-meta-v1.json.gz +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 + +[aurblup] +db-path = YOUR_AUR_ROOT/aurblup/ diff --git a/doc/git-interface.txt b/doc/git-interface.txt index f9409612..8c6806f7 100644 --- a/doc/git-interface.txt +++ b/doc/git-interface.txt @@ -111,3 +111,21 @@ 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. + +Script Wrappers (poetry) +------------------------ + +When aurweb is installed within a poetry virtualenv, commands need +to be prefixed with `poetry run`. A few wrapper scripts are provided +which automate this, as long as the executing user's `$HOME` is +aurweb's project root. + +- `examples/aurweb-git-auth.sh` +- `examples/aurweb-git-serve.sh` +- `examples/aurweb-git-update.sh` + +These scripts should be installed somewhere on the aurweb host. The +following options need to be updated to the installed wrappers: + +- `[auth] git-serve-cmd` +- `[serve] git-update-cmd` diff --git a/doc/i18n.txt b/doc/i18n.txt index 8216c084..44fb0f1f 100644 --- a/doc/i18n.txt +++ b/doc/i18n.txt @@ -21,7 +21,7 @@ strings for the translation to be usable, and it may have to be disabled. 1. Check out the aurweb source using git: -$ git clone git://git.archlinux.org/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 create a initial translation file from our translation catalog: diff --git a/doc/maintenance.txt b/doc/maintenance.txt index 2c5c9faf..fbde1fff 100644 --- a/doc/maintenance.txt +++ b/doc/maintenance.txt @@ -76,16 +76,16 @@ computations and clean up the database: * aurweb-usermaint removes the last login IP address of all users that did not login within the past seven days. -These scripts can be installed by running `python3 setup.py install` and are +These scripts can be installed by running `poetry install` and are usually scheduled using Cron. The current setup is: ---- -*/5 * * * * aurweb-mkpkglists [--extended] -1 */2 * * * aurweb-popupdate -2 */2 * * * aurweb-aurblup -3 */2 * * * aurweb-pkgmaint -4 */2 * * * aurweb-usermaint -5 */12 * * * aurweb-tuvotereminder +*/5 * * * * poetry run aurweb-mkpkglists [--extended] +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 ---- Advanced Administrative Features diff --git a/doc/rpc.txt b/doc/rpc.txt index 3148ebea..b0f5c4e1 100644 --- a/doc/rpc.txt +++ b/doc/rpc.txt @@ -39,6 +39,10 @@ Examples `/rpc/?v=5&type=search&by=makedepends&arg=boost` `search` with callback:: `/rpc/?v=5&type=search&arg=foobar&callback=jsonp1192244621103` +`search` with API Version 6 for packages containing `cookie` AND `milk`:: + `/rpc/?v=6&type=search&arg=cookie%20milk` +`search` with API Version 6 for packages containing `cookie milk`:: + `/rpc/?v=6&type=search&arg="cookie milk"` `info`:: `/rpc/?v=5&type=info&arg[]=foobar` `info` with multiple packages:: diff --git a/doc/sso.txt b/doc/sso.txt new file mode 100644 index 00000000..481166cf --- /dev/null +++ b/doc/sso.txt @@ -0,0 +1,38 @@ +Single Sign-On (SSO) +==================== + +This guide will walk you through setting up Keycloak for use with aurweb. For +extensive documentation, see . + +Installing Keycloak +------------------- + +Keycloak is in the official Arch repositories: + + # pacman -S keycloak + +The default port is 8080, which conflicts with aurweb’s default port. You need +to edit `/etc/keycloak/standalone.xml`, looking for this line: + + + +The default developer configuration assumes it is set to 8083. Alternatively, +you may customize [options] aur_location and [sso] openid_configuration in +`conf/config`. + +You may then start `keycloak.service` through systemd. + +See also ArchWiki . + +Configuring a realm +------------------- + +Go to and log in as administrator. Then, hover the +text right below the Keycloak logo at the top left, by default *Master*. Click +*Add realm* and name it *aurweb*. + +Open the *Clients* tab, and create a new *openid-connect* client. Call it +*aurweb*, and set the root URL to (your aur_location). + +Create a user from the *Users* tab and try logging in from +. diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml new file mode 100644 index 00000000..0b91dd93 --- /dev/null +++ b/docker-compose.aur-dev.yml @@ -0,0 +1,84 @@ +version: "3.8" + +services: + ca: + volumes: + - data:/data + - step:/root/.step + + memcached: + restart: always + + redis: + restart: always + + mariadb: + restart: always + + git: + restart: always + environment: + - AUR_CONFIG=/aurweb/conf/config + # SSH_CMDLINE should be updated to production's ssh cmdline. + - SSH_CMDLINE=${SSH_CMDLINE:-ssh ssh://aur@localhost:2222} + volumes: + - ${GIT_DATA_DIR}:/aurweb/aur.git + - data:/aurweb/data + + smartgit: + restart: always + volumes: + - ${GIT_DATA_DIR}:/aurweb/aur.git + - data:/data + - smartgit_run:/var/run/smartgit + + cgit-php: + restart: always + volumes: + - ${GIT_DATA_DIR}:/aurweb/aur.git + + cgit-fastapi: + restart: always + volumes: + - ${GIT_DATA_DIR}:/aurweb/aur.git + + cron: + volumes: + # Exclude ./aurweb:/aurweb in production. + - 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: + - COMMIT_HASH=$COMMIT_HASH + - FASTAPI_BACKEND="gunicorn" + - FASTAPI_WORKERS=${FASTAPI_WORKERS} + - AURWEB_FASTAPI_PREFIX=${AURWEB_FASTAPI_PREFIX} + - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus + volumes: + - data:/data + + nginx: + restart: always + volumes: + - data:/data + - archives:/var/lib/aurweb/archives + - smartgit_run:/var/run/smartgit + +volumes: + mariadb_run: {} # Share /var/run/mysqld + mariadb_data: {} # Share /var/lib/mysql + git_data: {} # Share aurweb/aur.git + smartgit_run: {} + data: {} + logs: {} diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 00000000..1e466730 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,54 @@ +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 + - ./data:/aurweb/data + + smartgit: + volumes: + - 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: + - ./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 + + nginx: + volumes: + - ./data:/data + - archives:/var/lib/aurweb/archives + - smartgit_run:/var/run/smartgit diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..ecfe964f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,357 @@ +# +# Docker service definitions for the aurweb project. +# +# Notable services: +# - `sharness` - Run sharness test suites +# - `pytest-mysql` - Run pytest suites with MariaDB +# - `pytest-sqlite` - Run pytest suites with SQLite +# - `test` - Run sharness, pytest-mysql and pytest-sqlite +# - `mariadb` - `port 13306` - MariaDB server for docker +# - `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. +# +# Copyright (C) 2021 aurweb Development +# All Rights Reserved. +version: "3.8" + +services: + aurweb-image: + build: . + image: aurweb:latest + + ca: + image: aurweb:latest + init: true + entrypoint: /docker/ca-entrypoint.sh + command: /docker/scripts/run-ca.sh + healthcheck: + test: "bash /docker/health/run-ca.sh" + interval: 2s + + 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 + entrypoint: /docker/redis-entrypoint.sh + command: /docker/scripts/run-redis.sh + healthcheck: + test: "bash /docker/health/redis.sh" + interval: 2s + ports: + - "127.0.0.1:16379:6379" + + mariadb: + image: aurweb:latest + init: true + entrypoint: /docker/mariadb-entrypoint.sh + command: /usr/bin/mysqld_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` + - "127.0.0.1:13306:3306" + volumes: + - mariadb_run:/var/run/mysqld # Bind socket in this volume. + - mariadb_data:/var/lib/mysql + healthcheck: + test: "bash /docker/health/mariadb.sh" + interval: 2s + + mariadb_init: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} + entrypoint: /docker/mariadb-init-entrypoint.sh + command: echo "MariaDB tables initialized." + volumes: + - mariadb_run:/var/run/mysqld + depends_on: + mariadb: + condition: service_healthy + + mariadb_test: + # Test database. + image: aurweb:latest + init: true + environment: + - MARIADB_PRIVILEGED=1 + entrypoint: /docker/mariadb-entrypoint.sh + command: /usr/bin/mysqld_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` + - "127.0.0.1:13307:3306" + volumes: + - mariadb_test_run:/var/run/mysqld # Bind socket in this volume. + healthcheck: + test: "bash /docker/health/mariadb.sh" + interval: 2s + + git: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=/aurweb/conf/config + - SSH_CMDLINE=${SSH_CMDLINE:-ssh ssh://aur@localhost:2222} + - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} + entrypoint: /docker/git-entrypoint.sh + command: /docker/scripts/run-sshd.sh + ports: + - "2222:2222" + healthcheck: + test: "bash /docker/health/sshd.sh" + interval: 2s + depends_on: + mariadb_init: + condition: service_started + volumes: + - mariadb_run:/var/run/mysqld + + smartgit: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=/aurweb/conf/config + entrypoint: /docker/smartgit-entrypoint.sh + 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 + depends_on: + git: + condition: service_healthy + ports: + - "127.0.0.1:13000:3000" + volumes: + - git_data:/aurweb/aur.git + + cgit-fastapi: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=/aurweb/conf/config + - CGIT_CLONE_PREFIX=${AURWEB_FASTAPI_PREFIX} + - CGIT_CSS=/static/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 + depends_on: + git: + condition: service_healthy + ports: + - "127.0.0.1:13001:3000" + volumes: + - git_data:/aurweb/aur.git + + cron: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=/aurweb/conf/config + entrypoint: /docker/cron-entrypoint.sh + command: /docker/scripts/run-cron.sh + depends_on: + mariadb_init: + condition: service_started + 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 + environment: + - AUR_CONFIG=conf/config + - FASTAPI_BACKEND=${FASTAPI_BACKEND} + - FASTAPI_WORKERS=${FASTAPI_WORKERS} + - AURWEB_FASTAPI_PREFIX=${AURWEB_FASTAPI_PREFIX} + - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus + - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} + entrypoint: /docker/fastapi-entrypoint.sh + command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" + healthcheck: + test: "bash /docker/health/fastapi.sh ${FASTAPI_BACKEND}" + interval: 2s + depends_on: + git: + condition: service_healthy + redis: + condition: service_healthy + cron: + condition: service_started + volumes: + - mariadb_run:/var/run/mysqld + ports: + - "127.0.0.1:18000:8000" + + nginx: + image: aurweb:latest + init: true + environment: + - AUR_CONFIG=conf/config + 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 + depends_on: + ca: + condition: service_started + cgit-php: + condition: service_healthy + cgit-fastapi: + condition: service_healthy + smartgit: + condition: service_healthy + fastapi: + condition: service_healthy + php-fpm: + condition: service_healthy + + sharness: + image: aurweb:latest + profiles: ["dev"] + init: true + environment: + - AUR_CONFIG=conf/config.sqlite + - LOG_CONFIG=logging.test.conf + entrypoint: /docker/sharness-entrypoint.sh + command: /docker/scripts/run-sharness.sh + stdin_open: true + tty: true + depends_on: + mariadb_test: + condition: service_healthy + 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 + + pytest-mysql: + image: aurweb:latest + profiles: ["dev"] + init: true + environment: + - AUR_CONFIG=conf/config + - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus + - LOG_CONFIG=logging.test.conf + entrypoint: /docker/test-mysql-entrypoint.sh + command: /docker/scripts/run-pytests.sh clean + stdin_open: true + tty: true + depends_on: + mariadb_test: + condition: service_healthy + tmpfs: + - /tmp + volumes: + - mariadb_test_run:/var/run/mysqld + - ./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 + + test: + image: aurweb:latest + profiles: ["dev"] + init: true + environment: + - AUR_CONFIG=conf/config + - TEST_RECURSION_LIMIT=${TEST_RECURSION_LIMIT} + - PROMETHEUS_MULTIPROC_DIR=/tmp_prometheus + - LOG_CONFIG=logging.test.conf + entrypoint: /docker/test-mysql-entrypoint.sh + command: /docker/scripts/run-tests.sh + stdin_open: true + tty: true + depends_on: + mariadb_test: + condition: service_healthy + volumes: + - mariadb_test_run:/var/run/mysqld + - ./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 + +volumes: + mariadb_test_run: {} + mariadb_run: {} # Share /var/run/mysqld/mysqld.sock + mariadb_data: {} # Share /var/lib/mysql + git_data: {} # Share aurweb/aur.git + smartgit_run: {} + archives: {} + step: {} diff --git a/docker/ca-entrypoint.sh b/docker/ca-entrypoint.sh new file mode 100755 index 00000000..d03efbbc --- /dev/null +++ b/docker/ca-entrypoint.sh @@ -0,0 +1,123 @@ +#!/bin/bash +# Initialize step-ca and request certificates from it. +# +# Certificates created by this service are meant to be used in +# aurweb Docker's nginx service. +# +# If ./data/root_ca.crt is present, CA generation is skipped. +# If ./data/${host}.{cert,key}.pem is available, host certificate +# generation is skipped. +# +set -eou pipefail + +# /data-based variables. +DATA_DIR="/data" +DATA_ROOT_CA="$DATA_DIR/root_ca.crt" +DATA_CERT="$DATA_DIR/localhost.cert.pem" +DATA_CERT_KEY="$DATA_DIR/localhost.key.pem" + +# Host certificates requested from the CA (separated by spaces). +DATA_CERT_HOSTS='localhost' + +# Local step paths and CA configuration values. +STEP_DIR="$(step-cli path)" +STEP_CA_CONFIG="$STEP_DIR/config/ca.json" +STEP_CA_ADDR='127.0.0.1:8443' +STEP_CA_URL='https://localhost:8443' +STEP_CA_PROVISIONER='admin@localhost' + +# Password file used for both --password-file and --provisioner-password-file. +STEP_PASSWD_FILE="$STEP_DIR/password.txt" + +# Hostnames supported by the CA. +STEP_CA_NAME='aurweb' +STEP_CA_DNS='localhost' + +make_password() { + # Create a random 20-length password and write it to $1. + openssl rand -hex 20 > $1 +} + +setup_step_ca() { + # Cleanup and setup step ca configuration. + rm -rf $STEP_DIR/* + + # Initialize `step` + make_password "$STEP_PASSWD_FILE" + step-cli ca init \ + --name="$STEP_CA_NAME" \ + --dns="$STEP_CA_DNS" \ + --address="$STEP_CA_ADDR" \ + --password-file="$STEP_PASSWD_FILE" \ + --provisioner="$STEP_CA_PROVISIONER" \ + --provisioner-password-file="$STEP_PASSWD_FILE" \ + --with-ca-url="$STEP_CA_URL" + + # Update ca.json max TLS certificate duration to a year. + update-step-config "$STEP_CA_CONFIG" + + # Install root_ca.crt as read/writable to /data/root_ca.crt. + install -m666 "$STEP_DIR/certs/root_ca.crt" "$DATA_ROOT_CA" +} + +start_step_ca() { + # Start the step-ca web server. + step-ca "$STEP_CA_CONFIG" \ + --password-file="$STEP_PASSWD_FILE" & + until printf "" 2>>/dev/null >>/dev/tcp/127.0.0.1/8443; do + sleep 1 + done +} + +kill_step_ca() { + # Stop the step-ca web server. + killall step-ca >/dev/null 2>&1 || /bin/true +} + +install_step_ca() { + # Install step-ca certificate authority to the system. + step-cli certificate install "$STEP_DIR/certs/root_ca.crt" +} + +step_cert_request() { + # Request a certificate from the step ca. + step-cli ca certificate \ + --not-after=8800h \ + --provisioner="$STEP_CA_PROVISIONER" \ + --provisioner-password-file="$STEP_PASSWD_FILE" \ + $1 $2 $3 + chmod 666 /data/${1}.*.pem +} + +if [ ! -f $DATA_ROOT_CA ]; then + setup_step_ca + install_step_ca +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 + +exec "$@" diff --git a/docker/cgit-entrypoint.sh b/docker/cgit-entrypoint.sh new file mode 100755 index 00000000..a44675e2 --- /dev/null +++ b/docker/cgit-entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -eou pipefail + +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|repo\.path=.*|repo.path=/aurweb/aur.git|' /etc/cgitrc +sed -ri "s|^(css)=.*$|\1=${CGIT_CSS}|" /etc/cgitrc + +exec "$@" diff --git a/docker/config/aurweb-cron b/docker/config/aurweb-cron new file mode 100644 index 00000000..21fd35dc --- /dev/null +++ b/docker/config/aurweb-cron @@ -0,0 +1,7 @@ +AUR_CONFIG='/aurweb/conf/config' +*/5 * * * * bash -c 'aurweb-mkpkglists --extended' +*/2 * * * * bash -c 'aurweb-aurblup' +*/2 * * * * bash -c 'aurweb-pkgmaint' +*/2 * * * * bash -c 'aurweb-usermaint' +*/2 * * * * bash -c 'aurweb-popupdate' +*/12 * * * * bash -c 'aurweb-tuvotereminder' diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf new file mode 100644 index 00000000..9fdf6015 --- /dev/null +++ b/docker/config/nginx.conf @@ -0,0 +1,150 @@ +daemon off; +user root; +worker_processes auto; +pid /var/run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 256; +} + +http { + sendfile on; + tcp_nopush on; + types_hash_max_size 4096; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + + access_log /dev/stdout; + error_log /dev/stderr; + + gzip on; + + upstream asgi_backend { + server fastapi:8000; + } + + upstream cgit-php { + server cgit-php:3000; + } + + upstream cgit-fastapi { + server cgit-fastapi:3000; + } + + upstream smartgit { + 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; + + ssl_certificate /etc/ssl/certs/web.cert.pem; + ssl_certificate_key /etc/ssl/private/web.key.pem; + + location ~ ^/[^\/]+\.gz$ { + # Override mime type to text/plain. + types { text/plain gz; } + default_type text/plain; + + # Filesystem location of .gz archives. + root /var/lib/aurweb/archives; + + # When we match this block, fix-up trying without a trailing slash. + try_files $uri $uri/ =404; + + # Caching headers. + expires max; + add_header Content-Encoding gzip; + add_header Cache-Control public; + add_header Last-Modified ""; + add_header ETag ""; + } + + 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-fastapi; + } + + location / { + proxy_pass http://asgi_backend; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Protocol ssl; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header X-Forwarded-Ssl on; + } + } + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } +} + diff --git a/docker/cron-entrypoint.sh b/docker/cron-entrypoint.sh new file mode 100755 index 00000000..5b69ab19 --- /dev/null +++ b/docker/cron-entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -eou pipefail + +# Setup the DB. +NO_INITDB=1 /docker/mariadb-init-entrypoint.sh + +# Create aurblup's directory. +AURBLUP_DIR="/aurweb/aurblup/" +mkdir -p $AURBLUP_DIR + +# Setup aurblup config for Docker. +AURBLUP_DBS='core extra community multilib testing community-testing' +AURBLUP_SERVER='https://mirrors.kernel.org/archlinux/%s/os/x86_64' +aurweb-config set aurblup db-path "$AURBLUP_DIR" +aurweb-config set aurblup sync-dbs "$AURBLUP_DBS" +aurweb-config set aurblup server "$AURBLUP_SERVER" + +# Setup mkpkglists config for Docker. +ARCHIVE_DIR='/var/lib/aurweb/archives' +aurweb-config set mkpkglists archivedir "$ARCHIVE_DIR" +aurweb-config set mkpkglists packagesfile "$ARCHIVE_DIR/packages.gz" +aurweb-config set mkpkglists packagesmetafile \ + "$ARCHIVE_DIR/packages-meta-v1.json.gz" +aurweb-config set mkpkglists packagesmetaextfile \ + "$ARCHIVE_DIR/packages-meta-ext-v1.json.gz" +aurweb-config set mkpkglists pkgbasefile "$ARCHIVE_DIR/pkgbase.gz" +aurweb-config set mkpkglists userfile "$ARCHIVE_DIR/users.gz" + +# Install the cron configuration. +cp /docker/config/aurweb-cron /etc/cron.d/aurweb-cron +chmod 0644 /etc/cron.d/aurweb-cron +crontab /etc/cron.d/aurweb-cron + +exec "$@" diff --git a/docker/fastapi-entrypoint.sh b/docker/fastapi-entrypoint.sh new file mode 100755 index 00000000..c6597313 --- /dev/null +++ b/docker/fastapi-entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -eou pipefail + +# Setup database. +NO_INITDB=1 /docker/mariadb-init-entrypoint.sh + +# Setup some other options. +aurweb-config set options cache 'redis' +aurweb-config set options redis_address 'redis://redis' +aurweb-config set options aur_location "$AURWEB_FASTAPI_PREFIX" +aurweb-config set options git_clone_uri_anon "${AURWEB_FASTAPI_PREFIX}/%s.git" +aurweb-config set options git_clone_uri_priv "${AURWEB_SSHD_PREFIX}/%s.git" + +if [ ! -z ${COMMIT_HASH+x} ]; then + aurweb-config set devel commit_hash "$COMMIT_HASH" +fi + +# Setup prometheus directory. +rm -rf $PROMETHEUS_MULTIPROC_DIR +mkdir -p $PROMETHEUS_MULTIPROC_DIR + +exec "$@" diff --git a/docker/git-entrypoint.sh b/docker/git-entrypoint.sh new file mode 100755 index 00000000..c9f1ec30 --- /dev/null +++ b/docker/git-entrypoint.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -eou pipefail + +SSHD_CONFIG=/etc/ssh/sshd_config +AUTH_SCRIPT=/app/git-auth.sh + +GIT_REPO=/aurweb/aur.git +GIT_BRANCH=master # 'Master' branch. + +if ! grep -q 'PYTHONPATH' /etc/environment; then + echo "PYTHONPATH='/aurweb:/aurweb/app'" >> /etc/environment +else + sed -ri "s|^(PYTHONPATH)=.*$|\1='/aurweb'|" /etc/environment +fi + +if ! grep -q 'AUR_CONFIG' /etc/environment; then + echo "AUR_CONFIG='/aurweb/conf/config'" >> /etc/environment +else + sed -ri "s|^(AUR_CONFIG)=.*$|\1='/aurweb/conf/config'|" /etc/environment +fi + +mkdir -p /app +chmod 755 /app + +cat >> $AUTH_SCRIPT << EOF +#!/usr/bin/env bash +export AUR_CONFIG="$AUR_CONFIG" +exec /usr/bin/aurweb-git-auth "\$@" +EOF +chmod 755 $AUTH_SCRIPT + +# Add AUR SSH config. +cat >> $SSHD_CONFIG << EOF +Match User aur + PasswordAuthentication no + AuthorizedKeysCommand $AUTH_SCRIPT "%t" "%k" + AuthorizedKeysCommandUser aur + AcceptEnv AUR_OVERWRITE +EOF + +# Setup database. +NO_INITDB=1 /docker/mariadb-init-entrypoint.sh + +# Setup some other options. +aurweb-config set serve repo-path '/aurweb/aur.git/' +aurweb-config set serve ssh-cmdline "$SSH_CMDLINE" + +# Setup SSH Keys. +ssh-keygen -A + +# In docker-compose.aur-dev.yml, we bind ./data to /aurweb/data. +# Production users wishing to include their own SSH keys should +# supply them in ./data. +if [ -d /aurweb/data ]; then + find /aurweb/data -type f -name 'ssh_host_*' -exec cp -vf "{}" /etc/ssh/ \; +fi + +# Taken from INSTALL. +mkdir -pv $GIT_REPO + +# Initialize git repository. +if [ ! -f $GIT_REPO/config ]; then + curdir="$(pwd)" + cd $GIT_REPO + git config --global init.defaultBranch $GIT_BRANCH + git init --bare + git config --local transfer.hideRefs '^refs/' + git config --local --add transfer.hideRefs '!refs/' + git config --local --add transfer.hideRefs '!HEAD' + ln -sf /usr/bin/aurweb-git-update hooks/update + cd $curdir + chown -R aur:aur $GIT_REPO +fi + +exec "$@" diff --git a/docker/health/ca.sh b/docker/health/ca.sh new file mode 100755 index 00000000..3e4bbe8e --- /dev/null +++ b/docker/health/ca.sh @@ -0,0 +1,2 @@ + +exec printf "" 2>>/dev/null >>/dev/tcp/127.0.0.1/8443 diff --git a/docker/health/cgit.sh b/docker/health/cgit.sh new file mode 100755 index 00000000..2f0cfeb1 --- /dev/null +++ b/docker/health/cgit.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec printf "" >>/dev/tcp/127.0.0.1/${1} diff --git a/docker/health/fastapi.sh b/docker/health/fastapi.sh new file mode 100755 index 00000000..e1df348d --- /dev/null +++ b/docker/health/fastapi.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec pgrep "$1" diff --git a/docker/health/mariadb.sh b/docker/health/mariadb.sh new file mode 100755 index 00000000..cbae37bd --- /dev/null +++ b/docker/health/mariadb.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec mysqladmin ping --silent diff --git a/docker/health/memcached.sh b/docker/health/memcached.sh new file mode 100755 index 00000000..00f8cd98 --- /dev/null +++ b/docker/health/memcached.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec pgrep memcached diff --git a/docker/health/nginx.sh b/docker/health/nginx.sh new file mode 100755 index 00000000..c530103d --- /dev/null +++ b/docker/health/nginx.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec curl --no-verify -q https://localhost:8444 diff --git a/docker/health/php.sh b/docker/health/php.sh new file mode 100755 index 00000000..7325946b --- /dev/null +++ b/docker/health/php.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec printf "" >>/dev/tcp/127.0.0.1/9000 diff --git a/docker/health/redis.sh b/docker/health/redis.sh new file mode 100755 index 00000000..b5b442e8 --- /dev/null +++ b/docker/health/redis.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec pgrep redis-server diff --git a/docker/health/smartgit.sh b/docker/health/smartgit.sh new file mode 100755 index 00000000..b4e7ebd4 --- /dev/null +++ b/docker/health/smartgit.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec pgrep uwsgi diff --git a/docker/health/sshd.sh b/docker/health/sshd.sh new file mode 100755 index 00000000..d9da9ea1 --- /dev/null +++ b/docker/health/sshd.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Opt to just pgrep sshd instead of connecting here. This health +# script is used on a regular interval and it ends up spamming +# the git service's logs with accesses. +exec pgrep sshd diff --git a/docker/hypercorn.env b/docker/hypercorn.env new file mode 100644 index 00000000..0d65e241 --- /dev/null +++ b/docker/hypercorn.env @@ -0,0 +1 @@ +FASTAPI_BACKEND="hypercorn" diff --git a/docker/localhost.ext b/docker/localhost.ext new file mode 100644 index 00000000..ab9de5fb --- /dev/null +++ b/docker/localhost.ext @@ -0,0 +1,7 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost diff --git a/docker/logging.conf b/docker/logging.conf new file mode 100644 index 00000000..ab754c9f --- /dev/null +++ b/docker/logging.conf @@ -0,0 +1,27 @@ +[loggers] +keys=root,sampleLogger + +[handlers] +keys=consoleHandler + +[formatters] +keys=sampleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + +[logger_sampleLogger] +level=DEBUG +handlers=consoleHandler +qualname=sampleLogger +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=sampleFormatter +args=(sys.stdout,) + +[formatter_sampleFormatter] +format=%(asctime)s - %(name)s - %(levelname)s - %(message)s diff --git a/docker/mariadb-entrypoint.sh b/docker/mariadb-entrypoint.sh new file mode 100755 index 00000000..a00f6106 --- /dev/null +++ b/docker/mariadb-entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/bash +set -eou pipefail + +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 + sleep 1s +done + +# Configure databases. +DATABASE="aurweb" # Persistent database for fastapi/php-fpm. + +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;" + +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'@'%';" + +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;" + +mysqladmin -uroot shutdown + +exec "$@" diff --git a/docker/mariadb-init-entrypoint.sh b/docker/mariadb-init-entrypoint.sh new file mode 100755 index 00000000..74980031 --- /dev/null +++ b/docker/mariadb-init-entrypoint.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -eou pipefail + +# Setup a config for our mysql db. +aurweb-config set database name 'aurweb' +aurweb-config set database user 'aur' +aurweb-config set database password 'aur' +aurweb-config set database host 'localhost' +aurweb-config set database socket '/var/run/mysqld/mysqld.sock' +aurweb-config unset database port + +if [ ! -z ${NO_INITDB+x} ]; then + exec "$@" +fi + +python -m aurweb.initdb 2>/dev/null || /bin/true +exec "$@" diff --git a/docker/nginx-entrypoint.sh b/docker/nginx-entrypoint.sh new file mode 100755 index 00000000..1527cda7 --- /dev/null +++ b/docker/nginx-entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -eou pipefail + +# If production.{cert,key}.pem exists, prefer them. This allows +# user customization of the certificates that FastAPI uses. +# Otherwise, fallback to localhost.{cert,key}.pem, generated by `ca`. + +CERT=/data/production.cert.pem +KEY=/data/production.key.pem + +DEST_CERT=/etc/ssl/certs/web.cert.pem +DEST_KEY=/etc/ssl/private/web.key.pem + +if [ -f "$CERT" ]; then + cp -vf "$CERT" "$DEST_CERT" + cp -vf "$KEY" "$DEST_KEY" +else + cat /data/localhost.cert.pem /data/root_ca.crt > "$DEST_CERT" + cp -vf /data/localhost.key.pem "$DEST_KEY" +fi + +cp -vf /docker/config/nginx.conf /etc/nginx/nginx.conf + +exec "$@" diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh new file mode 100755 index 00000000..dc1a91de --- /dev/null +++ b/docker/php-entrypoint.sh @@ -0,0 +1,32 @@ +#!/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 new file mode 100755 index 00000000..e92be6c5 --- /dev/null +++ b/docker/redis-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -eou pipefail + +sed -ri 's/^bind .*$/bind 0.0.0.0 -::1/g' /etc/redis/redis.conf + +exec "$@" diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh new file mode 100755 index 00000000..ced18c81 --- /dev/null +++ b/docker/scripts/install-deps.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Install Arch Linux dependencies. This is centralized here +# for CI and Docker usage and should always reflect the most +# robust development ecosystem. +set -eou pipefail + +# Update and rollout archlinux-keyring keys. +pacman-key --init +pacman-key --updatedb +pacman-key --populate + +pacman -Sy --noconfirm --noprogressbar archlinux-keyring + +# Install other OS dependencies. +pacman -Syu --noconfirm --noprogressbar \ + --cachedir .pkg-cache 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 + +exec "$@" diff --git a/docker/scripts/install-python-deps.sh b/docker/scripts/install-python-deps.sh new file mode 100755 index 00000000..3d5f28f0 --- /dev/null +++ b/docker/scripts/install-python-deps.sh @@ -0,0 +1,11 @@ +#!/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 +poetry install --no-interaction --no-ansi + +exec "$@" diff --git a/docker/scripts/run-ca.sh b/docker/scripts/run-ca.sh new file mode 100755 index 00000000..1ef45ef7 --- /dev/null +++ b/docker/scripts/run-ca.sh @@ -0,0 +1,7 @@ +#!/bin/bash +STEP_DIR="$(step-cli path)" +STEP_PASSWD_FILE="$STEP_DIR/password.txt" +STEP_CA_CONFIG="$STEP_DIR/config/ca.json" + +# Start the step-ca https server. +exec step-ca "$STEP_CA_CONFIG" --password-file="$STEP_PASSWD_FILE" diff --git a/docker/scripts/run-cgit.sh b/docker/scripts/run-cgit.sh new file mode 100755 index 00000000..5551a568 --- /dev/null +++ b/docker/scripts/run-cgit.sh @@ -0,0 +1,4 @@ +#!/bin/bash +exec uwsgi --socket 0.0.0.0:${1} \ + --plugins cgi \ + --cgi /usr/share/webapps/cgit-aurweb/cgit.cgi diff --git a/docker/scripts/run-cron.sh b/docker/scripts/run-cron.sh new file mode 100755 index 00000000..03bc55ff --- /dev/null +++ b/docker/scripts/run-cron.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +cd /aurweb +aurweb-aurblup +if [ $? -eq 0 ]; then + echo "[$(date -u)] executed aurblup" >> /var/log/aurblup.log +fi + +aurweb-mkpkglists --extended +if [ $? -eq 0 ]; then + echo "[$(date -u)] executed mkpkglists" >> /var/log/mkpkglists.log +fi + +exec /usr/bin/crond -nx proc diff --git a/docker/scripts/run-fastapi.sh b/docker/scripts/run-fastapi.sh new file mode 100755 index 00000000..fe037542 --- /dev/null +++ b/docker/scripts/run-fastapi.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# By default, set FASTAPI_WORKERS to 2. In production, this should +# be configured by the deployer. +if [ -z ${FASTAPI_WORKERS+x} ]; then + FASTAPI_WORKERS=2 +fi + +export FASTAPI_BACKEND="$1" + +echo "FASTAPI_BACKEND: $FASTAPI_BACKEND" +echo "FASTAPI_WORKERS: $FASTAPI_WORKERS" + +# Perform migrations. +alembic upgrade head + +if [ "$1" == "uvicorn" ] || [ "$1" == "" ]; then + exec uvicorn --reload \ + --log-config /docker/logging.conf \ + --host "0.0.0.0" \ + --port 8000 \ + --forwarded-allow-ips "*" \ + aurweb.asgi:app +elif [ "$1" == "gunicorn" ]; then + exec gunicorn \ + --log-config /docker/logging.conf \ + --bind "0.0.0.0:8000" \ + --proxy-protocol \ + --forwarded-allow-ips "*" \ + -w $FASTAPI_WORKERS \ + -k uvicorn.workers.UvicornWorker \ + aurweb.asgi:app +elif [ "$1" == "hypercorn" ]; then + exec hypercorn --reload \ + --log-config /docker/logging.conf \ + -b "0.0.0.0:8000" \ + --forwarded-allow-ips "*" \ + aurweb.asgi:app +else + echo "Error: Invalid \$FASTAPI_BACKEND supplied." + echo "Valid backends: 'uvicorn', 'gunicorn', 'hypercorn'." + exit 1 +fi diff --git a/docker/scripts/run-memcached.sh b/docker/scripts/run-memcached.sh new file mode 100755 index 00000000..90784b0f --- /dev/null +++ b/docker/scripts/run-memcached.sh @@ -0,0 +1,2 @@ +#!/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 new file mode 100755 index 00000000..6ece3303 --- /dev/null +++ b/docker/scripts/run-nginx.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +echo "=== Running nginx server! ===" +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 +echo " Thanks for using aurweb!" +echo + +exec nginx -c /etc/nginx/nginx.conf diff --git a/docker/scripts/run-php.sh b/docker/scripts/run-php.sh new file mode 100755 index 00000000..b86f8ce5 --- /dev/null +++ b/docker/scripts/run-php.sh @@ -0,0 +1,4 @@ +#!/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 new file mode 100755 index 00000000..d8c093d5 --- /dev/null +++ b/docker/scripts/run-pytests.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +COVERAGE=1 +PARAMS=() + +while [ $# -ne 0 ]; do + key="$1" + case "$key" in + --no-coverage) + COVERAGE=0 + shift + ;; + clean) + rm -f .coverage + shift + ;; + *) + echo "usage: $0 [--no-coverage] targets ..." + exit 1 + ;; + esac +done + +rm -rf $PROMETHEUS_MULTIPROC_DIR +mkdir -p $PROMETHEUS_MULTIPROC_DIR + +# Run pytest with optional targets in front of it. +pytest + +# By default, report coverage and move it into cache. +if [ $COVERAGE -eq 1 ]; then + make -C test coverage || /bin/true + + # /data is mounted as a volume. Copy coverage into it. + # Users can then sanitize the coverage locally in their + # aurweb root directory: ./util/fix-coverage ./data/.coverage + rm -f /data/.coverage + cp -v .coverage /data/.coverage + chmod 666 /data/.coverage +fi diff --git a/docker/scripts/run-redis.sh b/docker/scripts/run-redis.sh new file mode 100755 index 00000000..8dc98b10 --- /dev/null +++ b/docker/scripts/run-redis.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/bin/redis-server /etc/redis/redis.conf diff --git a/docker/scripts/run-sharness.sh b/docker/scripts/run-sharness.sh new file mode 100755 index 00000000..fe16751c --- /dev/null +++ b/docker/scripts/run-sharness.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -eou pipefail + +# Initialize the new database; ignore errors. +python -m aurweb.initdb 2>/dev/null || /bin/true + +eatmydata -- make -C test sh diff --git a/docker/scripts/run-smartgit.sh b/docker/scripts/run-smartgit.sh new file mode 100755 index 00000000..b6869a6c --- /dev/null +++ b/docker/scripts/run-smartgit.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +exec uwsgi \ + --socket /var/run/smartgit/smartgit.sock \ + --uid root \ + --gid http \ + --chmod-socket=666 \ + --plugins cgi \ + --cgi /usr/lib/git-core/git-http-backend diff --git a/docker/scripts/run-sshd.sh b/docker/scripts/run-sshd.sh new file mode 100755 index 00000000..d488e80d --- /dev/null +++ b/docker/scripts/run-sshd.sh @@ -0,0 +1,2 @@ +#!/bin/bash +exec /usr/sbin/sshd -e -p 2222 -D diff --git a/docker/scripts/run-tests.sh b/docker/scripts/run-tests.sh new file mode 100755 index 00000000..a726c957 --- /dev/null +++ b/docker/scripts/run-tests.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -eou pipefail +dir=$(dirname $0) + +# Clean up coverage and stuff. +make -C test clean + +# Run sharness tests. +bash $dir/run-sharness.sh + +# Run Python tests with MariaDB database. +# Pass --silence to avoid reporting coverage. We will do that below. +bash $dir/run-pytests.sh --no-coverage + +make -C test coverage + +# /data is mounted as a volume. Copy coverage into it. +# Users can then sanitize the coverage locally in their +# aurweb root directory: ./util/fix-coverage ./data/.coverage +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 diff --git a/docker/scripts/update-step-config b/docker/scripts/update-step-config new file mode 100755 index 00000000..bbdb2680 --- /dev/null +++ b/docker/scripts/update-step-config @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +import json +import sys + +CA_CONFIG = sys.argv[1] + +with open(CA_CONFIG) as f: + data = json.load(f) + +if "authority" not in data: + data["authority"] = dict() +if "claims" not in data["authority"]: + data["authority"]["claims"] = dict() + +# One year of certificate duration. +data["authority"]["claims"] = {"maxTLSCertDuration": "8800h"} + +with open(CA_CONFIG, "w") as f: + json.dump(data, f) diff --git a/docker/sharness-entrypoint.sh b/docker/sharness-entrypoint.sh new file mode 100755 index 00000000..daa9edeb --- /dev/null +++ b/docker/sharness-entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -eou pipefail + +exec "$@" diff --git a/docker/smartgit-entrypoint.sh b/docker/smartgit-entrypoint.sh new file mode 100755 index 00000000..daa9edeb --- /dev/null +++ b/docker/smartgit-entrypoint.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -eou pipefail + +exec "$@" diff --git a/docker/test-mysql-entrypoint.sh b/docker/test-mysql-entrypoint.sh new file mode 100755 index 00000000..b3464256 --- /dev/null +++ b/docker/test-mysql-entrypoint.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -eou pipefail + +# We use the root user for testing in Docker. +# The test user must be able to create databases and drop them. +aurweb-config set database user 'root' +aurweb-config set database host 'localhost' +aurweb-config set database socket '/var/run/mysqld/mysqld.sock' + +# Remove possibly problematic configuration options. +# We depend on the database socket within Docker and +# being run as the root user. +aurweb-config unset database password +aurweb-config unset database port + +# Setup notifications for testing. +aurweb-config set notifications sendmail "$(pwd)/util/sendmail" + +exec "$@" diff --git a/docker/tests-entrypoint.sh b/docker/tests-entrypoint.sh new file mode 100755 index 00000000..145bee6e --- /dev/null +++ b/docker/tests-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -eou pipefail +dir="$(dirname $0)" + +bash $dir/test-mysql-entrypoint.sh + +exec "$@" diff --git a/examples/aurweb-git-auth.sh b/examples/aurweb-git-auth.sh new file mode 100644 index 00000000..3e4defd4 --- /dev/null +++ b/examples/aurweb-git-auth.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Wrapper script used to call aurweb-git-update externally when +# utilizing an app-based virtualenv. +aurweb_dir="$HOME" +cd $aurweb_dir +exec poetry run aurweb-git-auth "$@" diff --git a/examples/aurweb-git-serve.sh b/examples/aurweb-git-serve.sh new file mode 100644 index 00000000..3a64dcbd --- /dev/null +++ b/examples/aurweb-git-serve.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Wrapper script used to call aurweb-git-update externally when +# utilizing an app-based virtualenv. +aurweb_dir="$HOME" +cd $aurweb_dir +exec poetry run aurweb-git-serve "$@" diff --git a/examples/aurweb-git-update.sh b/examples/aurweb-git-update.sh new file mode 100644 index 00000000..808dbccd --- /dev/null +++ b/examples/aurweb-git-update.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Wrapper script used to call aurweb-git-update externally when +# utilizing an app-based virtualenv. +aurweb_dir="$HOME" +cd $aurweb_dir +exec poetry run aurweb-git-update "$@" diff --git a/examples/aurweb.service b/examples/aurweb.service new file mode 100644 index 00000000..9c482030 --- /dev/null +++ b/examples/aurweb.service @@ -0,0 +1,16 @@ +[Unit] +Description=aurweb asgi server + +[Description] +User=aur +WorkingDirectory=/srv/http/aurweb +ExecStart=/usr/bin/poetry run gunicorn \ + --log-config /srv/http/aurweb/logging.conf \ + --bind '0.0.0.0:8000' \ + --forwarded-allow-ips '*' \ + --workers 4 \ + -k uvicorn.workers.UvicornWorker \ + aurweb.asgi:app + +[Install] +WantedBy=multi-user.target diff --git a/examples/jsonp.html b/examples/jsonp.html new file mode 100644 index 00000000..d73ec91e --- /dev/null +++ b/examples/jsonp.html @@ -0,0 +1,74 @@ + + + + + + + + JSONP Callback Test + + + + + +
+
+

+ Searching with the following form uses a JSONP callback + to log data out to the javascript console. +

+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
+ + diff --git a/logging.conf b/logging.conf new file mode 100644 index 00000000..7dfd30f0 --- /dev/null +++ b/logging.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=INFO +handlers=simpleHandler +qualname=uvicorn +propagate=0 + +[logger_hypercorn] +level=INFO +handlers=simpleHandler +qualname=hypercorn +propagate=0 + +[logger_alembic] +level=INFO +handlers=simpleHandler +qualname=alembic +propagate=0 + +[handler_simpleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_detailedHandler] +class=StreamHandler +level=DEBUG +formatter=detailedFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s %(levelname)-5s | %(name)s: %(message)s +datefmt=%H:%M:%S + +[formatter_detailedFormatter] +format=%(asctime)s %(levelname)-5s | %(name)s.%(funcName)s() @ L%(lineno)d: %(message)s +datefmt=%H:%M:%S diff --git a/logging.test.conf b/logging.test.conf new file mode 100644 index 00000000..6c44f5dc --- /dev/null +++ b/logging.test.conf @@ -0,0 +1,60 @@ +[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] +; This loglevel is set to DEBUG for tests where we expect specific +; debug logging to occur. In production, this should be set to INFO. +level=DEBUG +handlers=simpleHandler +qualname=aurweb +propagate=1 + +[logger_uvicorn] +level=INFO +handlers=simpleHandler +qualname=uvicorn +propagate=0 + +[logger_hypercorn] +level=INFO +handlers=simpleHandler +qualname=hypercorn +propagate=0 + +[logger_alembic] +level=INFO +handlers=simpleHandler +qualname=alembic +propagate=0 + +[handler_simpleHandler] +class=StreamHandler +level=DEBUG +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_detailedHandler] +class=StreamHandler +level=DEBUG +formatter=detailedFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s %(levelname)-5s | %(name)s: %(message)s +datefmt=%H:%M:%S + +[formatter_detailedFormatter] +format=%(asctime)s %(levelname)-5s | %(name)s.%(funcName)s() @ L%(lineno)d: %(message)s +datefmt=%H:%M:%S diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/migrations/env.py b/migrations/env.py index 1627e693..774ecdeb 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -1,19 +1,17 @@ -import aurweb.db -import aurweb.schema - -from alembic import context +import logging import logging.config + import sqlalchemy +from alembic import context + +import aurweb.db +import aurweb.schema # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config -# Interpret the config file for Python logging. -# This line sets up loggers basically. -logging.config.fileConfig(config.config_file_name) - # model MetaData for autogenerating migrations target_metadata = aurweb.schema.metadata @@ -22,6 +20,14 @@ target_metadata = aurweb.schema.metadata # my_important_option = config.get_main_option("my_important_option") # ... etc. +# If configure_logger is either True or not specified, +# configure the logger via fileConfig. +if config.attributes.get("configure_logger", True): + logging.config.fileConfig(config.config_file_name) + +# This grabs the root logger in env.py. +logger = logging.getLogger(__name__) + def run_migrations_offline(): """Run migrations in 'offline' mode. @@ -35,6 +41,8 @@ def run_migrations_offline(): script output. """ + dbname = aurweb.db.name() + logging.info(f"Performing offline migration on database '{dbname}'.") context.configure( url=aurweb.db.get_sqlalchemy_url(), target_metadata=target_metadata, @@ -53,6 +61,8 @@ def run_migrations_online(): and associate a connection with the context. """ + dbname = aurweb.db.name() + logging.info(f"Performing online migration on database '{dbname}'.") connectable = sqlalchemy.create_engine( aurweb.db.get_sqlalchemy_url(), poolclass=sqlalchemy.pool.NullPool, diff --git a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py index 03982676..c3b79dab 100644 --- a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py +++ b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py @@ -56,14 +56,14 @@ db_backend = aurweb.config.get("database", "backend") def rebuild_unique_indexes_with_str_cols(): for idx_name in indexes: sql = f""" -DROP INDEX IF EXISTS {idx_name} +DROP INDEX IF EXISTS {idx_name} ON {indexes.get(idx_name)[0]} """ op.execute(sql) sql = f""" -CREATE UNIQUE INDEX {idx_name} -ON {indexes.get(idx_name)[0]} -({indexes.get(idx_name)[1]}, {indexes.get(idx_name)[2]}) +CREATE UNIQUE INDEX {idx_name} +ON {indexes.get(idx_name)[0]} +({indexes.get(idx_name)[1]}, {indexes.get(idx_name)[2]}) """ op.execute(sql) @@ -77,8 +77,8 @@ def upgrade(): def op_execute(table_meta): table, charset, collate = table_meta sql = f""" -ALTER TABLE {table} -CONVERT TO CHARACTER SET {charset} +ALTER TABLE {table} +CONVERT TO CHARACTER SET {charset} COLLATE {collate} """ op.execute(sql) @@ -94,8 +94,8 @@ def downgrade(): def op_execute(table_meta): table, charset, collate = table_meta sql = f""" -ALTER TABLE {table} -CONVERT TO CHARACTER SET {src_charset} +ALTER TABLE {table} +CONVERT TO CHARACTER SET {src_charset} COLLATE {src_collate} """ op.execute(sql) diff --git a/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py b/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py new file mode 100644 index 00000000..d910a14b --- /dev/null +++ b/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py @@ -0,0 +1,49 @@ +""" +upgrade voteinfo integers + +Within `aurweb/schema.py`, these were previously forced to use +TINYINT(3, unsigned=True) types. When generating with dummy data, +it is very easy to bypass 3-character TU counts; in addition, +this is possible in the future on production, and so we should +deal with this case regardless. + +All previous TINYINT(3, unsigned=True) typed columns are upgraded +INTEGER(unsigned=True) types, supporting up to 4-bytes of active TUs +and votes for one particular proposal. + +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' +branch_labels = None +depends_on = None + +# Upgrade to INTEGER(unsigned=True); supports 4-byte values. +UPGRADE_T = INTEGER(unsigned=True) + +# Downgrade to TINYINT(3, unsigned=True); supports 1-byte values. +DOWNGRADE_T = TINYINT(3, unsigned=True) + + +def upgrade(): + """ 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) + op.alter_column("TU_VoteInfo", "ActiveTUs", type_=UPGRADE_T) + + +def downgrade(): + """ + Downgrade 'Yes', 'No', 'Abstain' and 'ActiveTUs' to unsigned TINYINT. + """ + op.alter_column("TU_VoteInfo", "ActiveTUs", type_=DOWNGRADE_T) + op.alter_column("TU_VoteInfo", "Abstain", type_=DOWNGRADE_T) + op.alter_column("TU_VoteInfo", "No", type_=DOWNGRADE_T) + op.alter_column("TU_VoteInfo", "Yes", type_=DOWNGRADE_T) 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 2b257e9d..49bf055a 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 @@ -6,6 +6,7 @@ Create Date: 2020-06-08 10:04:13.898617 """ import sqlalchemy as sa + from alembic import op from sqlalchemy.engine.reflection import Inspector diff --git a/migrations/versions/f47cad5d6d03_initial_revision.py b/migrations/versions/f47cad5d6d03_initial_revision.py index 9e99490f..b214beea 100644 --- a/migrations/versions/f47cad5d6d03_initial_revision.py +++ b/migrations/versions/f47cad5d6d03_initial_revision.py @@ -1,14 +1,9 @@ """initial revision Revision ID: f47cad5d6d03 -Revises: Create Date: 2020-02-23 13:23:32.331396 """ -from alembic import op -import sqlalchemy as sa - - # revision identifiers, used by Alembic. revision = 'f47cad5d6d03' down_revision = None diff --git a/po/ar.po b/po/ar.po index 7664c478..676a5025 100644 --- a/po/ar.po +++ b/po/ar.po @@ -10,8 +10,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -472,6 +472,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 "تعذّر العثور على الحزمة لدمج التّصويتات والتّعليقات معها." @@ -570,6 +576,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 "" @@ -866,6 +880,10 @@ msgstr "استمارة الولوج معطّلة حاليًّا لعنوان IP 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 "" @@ -951,6 +969,30 @@ msgstr "خطأ في استرجاع تفاصيل الحزمة." msgid "Package details could not be found." msgstr "تعذّر العثور على تفاصيل الحزمة." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "عليك الولوج قبل تعليم الحزم." @@ -987,6 +1029,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 "حُذفت الحزم المحدّدة." @@ -995,10 +1041,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 "لم تحدّد أيّ حزم لتبنّيها." @@ -2256,3 +2310,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/aurweb.pot b/po/aurweb.pot index aeed9f02..afad9e12 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -471,6 +471,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 "" @@ -568,6 +574,15 @@ 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 "" @@ -864,6 +879,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 "" @@ -949,6 +968,30 @@ msgstr "" msgid "Package details could not be found." msgstr "" +#: 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 msgid "You must be logged in before you can flag packages." msgstr "" @@ -985,6 +1028,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 "" @@ -993,10 +1040,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 "" @@ -2233,3 +2288,49 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/az.po b/po/az.po new file mode 100644 index 00000000..7e534b4c --- /dev/null +++ b/po/az.po @@ -0,0 +1,2335 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: az\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/az_AZ.po b/po/az_AZ.po new file mode 100644 index 00000000..e903027b --- /dev/null +++ b/po/az_AZ.po @@ -0,0 +1,2335 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: az_AZ\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/bg.po b/po/bg.po new file mode 100644 index 00000000..7864f5dc --- /dev/null +++ b/po/bg.po @@ -0,0 +1,2335 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: bg\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/ca.po b/po/ca.po index c30fb1d8..391dd146 100644 --- a/po/ca.po +++ b/po/ca.po @@ -5,14 +5,15 @@ # Translators: # Adolfo Jayme-Barrientos, 2014 # Hector Mtz-Seara , 2011,2013 +# Ícar , 2021 # Lukas Fleischer , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -30,38 +31,38 @@ msgstr "Ho sentim, la pàgina que ha sol·licitat no existeix." #: html/404.php template/pkgreq_close_form.php msgid "Note" -msgstr "" +msgstr "Nota" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "" +msgstr "Les URL de clonació del Git no estan destinades a ser obertes en un navegador." #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "" +msgstr "Per clonar el repositori Git de %s, executeu %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "" +msgstr "Clica %saquí%s per tornar a la pàgina de detalls de %s." #: html/503.php msgid "Service Unavailable" -msgstr "" +msgstr "Servei no disponible" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "" +msgstr "No pateixis! Aquest lloc està caigut a causa d'un manteniment. Tornarem aviat." #: html/account.php msgid "Account" -msgstr "" +msgstr "Compte" #: html/account.php template/header.php msgid "Accounts" -msgstr "Compte" +msgstr "Comptes" #: html/account.php html/addvote.php msgid "You are not allowed to access this area." @@ -77,7 +78,7 @@ msgstr "No teniu permís per a editar aquest compte." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Contrasenya no vàlida." #: html/account.php msgid "Use this form to search existing accounts." @@ -135,19 +136,19 @@ msgstr "Tipus" #: html/addvote.php msgid "Addition of a TU" -msgstr "" +msgstr "Adició d'un TU" #: html/addvote.php msgid "Removal of a TU" -msgstr "" +msgstr "Elimincació d'un TU" #: html/addvote.php msgid "Removal of a TU (undeclared inactivity)" -msgstr "" +msgstr "Elimincació d'un TU (inactivitat no declarada)" #: html/addvote.php msgid "Amendment of Bylaws" -msgstr "" +msgstr "Modificació de les lleis" #: html/addvote.php template/tu_list.php msgid "Proposal" @@ -159,15 +160,15 @@ msgstr "Envia" #: html/comaintainers.php template/comaintainers_form.php msgid "Manage Co-maintainers" -msgstr "" +msgstr "Gestiona els co-mantenidors" #: html/commentedit.php template/pkg_comments.php msgid "Edit comment" -msgstr "" +msgstr "Edita el comentari" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Tauler" #: html/home.php template/header.php msgid "Home" @@ -175,11 +176,11 @@ msgstr "Inici" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "Els meus paquets marcats" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Les meves peticions" #: html/home.php msgid "My Packages" @@ -187,15 +188,15 @@ msgstr "Els meus paquets" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Cerca paquets que mantinc" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Paquets co-mantinguts" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Cerca paquets que co-mantinc" #: html/home.php #, php-format @@ -227,68 +228,68 @@ msgstr "Avís legal" msgid "" "AUR packages are user produced content. Any use of the provided files is at " "your own risk." -msgstr "" +msgstr "Els paquets de l'AUR són contingut produït per usuaris. Qualsevol ús dels fitxers proporcionats és sota el seu propi risc." #: html/home.php msgid "Learn more..." -msgstr "" +msgstr "Més informació..." #: html/home.php msgid "Support" -msgstr "" +msgstr "Suport" #: html/home.php msgid "Package Requests" -msgstr "" +msgstr "Sol·licituds de paquets" #: html/home.php #, php-format msgid "" "There are three types of requests that can be filed in the %sPackage " "Actions%s box on the package details page:" -msgstr "" +msgstr "Hi ha tres tipus de sol·licituds que es poden presentar al quadre %sAccions del paquet%s a la pàgina de detalls del paquet:" #: html/home.php msgid "Orphan Request" -msgstr "" +msgstr "Sol·licitud d'orfenat" #: 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 "" +msgstr "Sol·licita que un paquet sigui renegat, per exemple quan el mantenidor estigui inactiu i el paquet s'ha marcat com a obsolet durant molt de temps." #: html/home.php msgid "Deletion Request" -msgstr "" +msgstr "Sol·licitud de supressió" #: 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." -msgstr "" +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." #: html/home.php msgid "Merge Request" -msgstr "" +msgstr "Sol·licitud de fusió" #: html/home.php 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 "" +msgstr "Sol·licita que un paquet es fusioni en un altre. Es pot utilitzar quan un paquet s'ha de reanomenar o substituir per un paquet dividit." #: 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 "" +msgstr "Si voleu debatre una sol·licitud, podeu utilitzar la llista de correu %saur-requests%s. No obstant això, si us plau, no utilitzeu aquesta llista per a enviar una petició." #: html/home.php msgid "Submitting Packages" -msgstr "" +msgstr "Enviar paquets" #: html/home.php #, php-format @@ -296,11 +297,11 @@ msgid "" "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." -msgstr "" +msgstr "Ara s'utilitza Git a sobre d'SSH per enviar paquets a l'AUR. Feu un cop d'ull a la secció \"%sSubmitting packages%s\" de la pàgina de l'ArchWIki de l'AUR per més detalls." #: html/home.php msgid "The following SSH fingerprints are used for the AUR:" -msgstr "" +msgstr "Les següents empremtes digitals d'SSH són usades per l'AUR:" #: html/home.php msgid "Discussion" @@ -312,7 +313,7 @@ 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 "" +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." #: html/home.php msgid "Bug Reporting" @@ -325,7 +326,7 @@ msgid "" "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 "" +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." #: html/home.php msgid "Package Search" @@ -333,7 +334,7 @@ msgstr "Cercar Paquets" #: html/index.php msgid "Adopt" -msgstr "" +msgstr "Adopta" #: html/index.php msgid "Vote" @@ -374,7 +375,7 @@ msgstr "Introduïu les credencials d'inici de sessió" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Nom d'usuari o adreça de correu primària" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -438,7 +439,7 @@ msgstr "La seva contrasenya s'ha restablert correctament." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Confirmeu el vostre nom d'usuari o l'adreça de correu electrònic primària:" #: html/passreset.php msgid "Enter your new password:" @@ -457,20 +458,26 @@ 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 heu oblidat el nom d'usuari i l'adreça de correu electrònic primària utilitzada al registrar-vos, si us plau envieu un missatge a la llista de correu %saur-general%s." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Entreu el vostre nom d'usuari o la vostra adreça de correu electrònic primària:" #: html/pkgbase.php msgid "Package Bases" -msgstr "" +msgstr "Bases del paquet" #: html/pkgbase.php msgid "" "The selected packages have not been disowned, check the confirmation " "checkbox." +msgstr "Els paquets seleccionats no han sigut renegats, marqueu la casella de confirmació." + +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." msgstr "" #: html/pkgbase.php lib/pkgreqfuncs.inc.php @@ -479,7 +486,7 @@ msgstr "No es pot trobar el paquet on combinar vots i comentaris." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot merge a package base with itself." -msgstr "" +msgstr "No es pot fusionar la base d'un paquet amb la mateixa." #: html/pkgbase.php msgid "" @@ -500,7 +507,7 @@ msgstr "Esborrar el paquet" msgid "" "Use this form to delete the package base %s%s%s and the following packages " "from the AUR: " -msgstr "" +msgstr "Utilitzeu aquest formulari per eliminar la base del paquet %s%s%s i els següents paquets de l'AUR:" #: html/pkgdel.php msgid "Deletion of a package is permanent. " @@ -571,6 +578,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 "" @@ -867,6 +882,10 @@ msgstr "El formulari de registre està deshabilitat per la seva adreça IP, prob 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 "" @@ -952,6 +971,30 @@ msgstr "No s'han pogut obtenir els detalls del paquet." msgid "Package details could not be found." msgstr "No s'han pogut trobar els detalls del paquet." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Heu d'identificar-vos abans de marcar paquets." @@ -988,6 +1031,10 @@ msgstr "No té permís per eliminar paquets." msgid "You did not select any packages to delete." msgstr "No heu seleccionat cap paquet per a esborrar." +#: 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 "Els paquets seleccionats s'han esborrat." @@ -996,10 +1043,18 @@ msgstr "Els paquets seleccionats s'han esborrat." msgid "You must be logged in before you can adopt packages." msgstr "Heu d'identificar-vos abans d'apropiar-se paquets." +#: 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 "Heu d'identificar-vos abans de desapropiàr-se paquets." +#: 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 "No heu seleccionat cap paquet per apropiar-se'n." @@ -2237,3 +2292,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/ca_ES.po b/po/ca_ES.po new file mode 100644 index 00000000..bad69bd1 --- /dev/null +++ b/po/ca_ES.po @@ -0,0 +1,2335 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ca_ES\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/cs.po b/po/cs.po index ef165358..b9bd739a 100644 --- a/po/cs.po +++ b/po/cs.po @@ -4,18 +4,21 @@ # # Translators: # Daniel Milde , 2017 +# Daniel Peukert , 2021 +# Daniel Peukert , 2021 # Jaroslav Lichtblau , 2015-2016 # Jaroslav Lichtblau , 2014 # Jiří Vírava , 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" "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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -29,7 +32,7 @@ msgstr "Stránka nenalezena" #: html/404.php msgid "Sorry, the page you've requested does not exist." -msgstr "Omlouváme se, požadovaná stránka neexistuje." +msgstr "Omlouváme se, ale požadovaná stránka neexistuje." #: html/404.php template/pkgreq_close_form.php msgid "Note" @@ -42,12 +45,12 @@ msgstr "URL adresy pro klonování Git repozitáře nejsou určeny k otevření #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "Pro naklonování Git repozitáře %s, spusťte %s." +msgstr "Pro naklonování Git repozitáře %s použijte příkaz %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "Klikněte %s zde %s pro návrat na %s stránku detailu." +msgstr "Klikněte %s zde %s pro návrat na stránku o balíčku %s. " #: html/503.php msgid "Service Unavailable" @@ -56,7 +59,7 @@ msgstr "Služba nedostupná" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "Nepanikařte! Na tomto webu dochází k údržbě. Brzy budeme zpět." +msgstr "Nepanikařte! Na tomto webu probíhá údržba. Brzy budeme zpět." #: html/account.php msgid "Account" @@ -72,23 +75,23 @@ msgstr "Zde nemáte povolený přístup." #: html/account.php msgid "Could not retrieve information for the specified user." -msgstr "Nelze obdržet informace pro vybraného uživatele." +msgstr "Načtení informací o daném uživateli se nezdařilo." #: html/account.php msgid "You do not have permission to edit this account." -msgstr "Nemáte oprávnění pro úpravu tohoto účtu." +msgstr "Pro úpravu tohoto účtu nemáte oprávnění. " #: 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." -msgstr "Pro vyhledání existujících účtů použíte tento formulář." +msgstr "Tento formulář použijte pro vyhledání existujících účtů." #: html/account.php msgid "You must log in to view user information." -msgstr "Musíte se přihlásit, pro zobrazení informací o uživateli." +msgstr "Pro zobrazení informací o uživateli se musíte přihlásit." #: html/addvote.php template/tu_list.php msgid "Add Proposal" @@ -105,7 +108,7 @@ msgstr "Uživatelské jméno neexistuje." #: html/addvote.php #, php-format msgid "%s already has proposal running for them." -msgstr "Pro %s se již hlasuje." +msgstr "Pro %s již existuje návrh." #: html/addvote.php msgid "Invalid type." @@ -113,23 +116,23 @@ msgstr "Chybný typ." #: html/addvote.php msgid "Proposal cannot be empty." -msgstr "Návrh nemůže být prázdný." +msgstr "Návrh nesmí být prázdný." #: html/addvote.php msgid "New proposal submitted." -msgstr "Nový návrh podán." +msgstr "Nový návrh předložen." #: html/addvote.php msgid "Submit a proposal to vote on." -msgstr "Předložit návrh na hlasování." +msgstr "Předložit návrh k hlasování." #: html/addvote.php msgid "Applicant/TU" -msgstr "Uchazeč/TU" +msgstr "Uchazeč/důvěryhodný uživatel" #: html/addvote.php msgid "(empty if not applicable)" -msgstr "(vynechat pokud neni vhodný)" +msgstr "(nevyhovující přeskočte)" #: html/addvote.php template/account_search_results.php #: template/pkgreq_results.php @@ -138,15 +141,15 @@ msgstr "Typ" #: html/addvote.php msgid "Addition of a TU" -msgstr "Přidání TU" +msgstr "Přidání důvěryhodného uživatele" #: html/addvote.php msgid "Removal of a TU" -msgstr "Odebrání TU" +msgstr "Odebrání důvěryhodného uživatele" #: html/addvote.php msgid "Removal of a TU (undeclared inactivity)" -msgstr "Odebrání TU (nehlášená neaktivita)" +msgstr "Odebrání důvěryhodného uživatele (neohlášená neaktivita)" #: html/addvote.php msgid "Amendment of Bylaws" @@ -162,7 +165,7 @@ msgstr "Odeslat" #: html/comaintainers.php template/comaintainers_form.php msgid "Manage Co-maintainers" -msgstr "Správa spolu-správců" +msgstr "Správa spolusprávců" #: html/commentedit.php template/pkg_comments.php msgid "Edit comment" @@ -170,11 +173,11 @@ msgstr "Upravit komentář" #: html/home.php template/header.php msgid "Dashboard" -msgstr "Dashboard" +msgstr "Nástěnka" #: html/home.php template/header.php msgid "Home" -msgstr "Domů" +msgstr "hlavní stránka" #: html/home.php msgid "My Flagged Packages" @@ -194,25 +197,25 @@ msgstr "Hledat balíčky, které spravuji" #: html/home.php msgid "Co-Maintained Packages" -msgstr "Spolu-spravované balíčky" +msgstr "Spoluspravované balíčky" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "Hledat balíčky, které spolu-spravuji" +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 "" +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." #: html/home.php #, php-format msgid "" "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " "otherwise they will be deleted!" -msgstr "" +msgstr "Nahrané soubory PKGBUILD %smusí%s dodržovat %sstandardy balíčků systému Arch%s, jinak budou smazány!" #: html/home.php msgid "Remember to vote for your favourite packages!" @@ -224,13 +227,13 @@ msgstr "Některé balíčky mohou být poskytnuty v binární podobě v repozit #: html/home.php msgid "DISCLAIMER" -msgstr "" +msgstr "UPOZORNĚNÍ" #: html/home.php template/footer.php msgid "" "AUR packages are user produced content. Any use of the provided files is at " "your own risk." -msgstr "Obsah balíčků AUR je vytvořen uživateli. Jakékoli použití poskytnutých souborů je na vlastní nebezpečí." +msgstr "Obsah balíčků v repozitáři AUR je tvořen uživateli. Použití zde nalezených souborů je na vlastní nebezpečí." #: html/home.php msgid "Learn more..." @@ -242,24 +245,24 @@ msgstr "Podpora" #: html/home.php msgid "Package Requests" -msgstr "Žádosti o balíčky" +msgstr "Žádosti související s balíčky" #: html/home.php #, php-format msgid "" "There are three types of requests that can be filed in the %sPackage " "Actions%s box on the package details page:" -msgstr "" +msgstr "Existují tři typy žádostí, které je možné vybrat v nabídce %sAkce balíčku%s na stránce balíčku:" #: html/home.php msgid "Orphan Request" -msgstr "Žádost o osiření" +msgstr "Žádost o odebrání vlastnictví" #: 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 "" +msgstr "Požádá o zrušení vlastnictví, např. pokud je správce neaktivní a balíček je již dlouho označen za neaktuální." #: html/home.php msgid "Deletion Request" @@ -270,7 +273,7 @@ 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 "" +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í." #: html/home.php msgid "Merge Request" @@ -280,18 +283,18 @@ msgstr "Žádost o sloučení" 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 "" +msgstr "Požádá o sloučení balíčku s jiným balíčkem. Může být použito, pokud je nutné balíček přejmenovat nebo nahradit rozděleným balíčkem." #: 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 "" +msgstr "Pokud chcete nějakou žádost prodiskutovat, použijte poštovní konferenci %saur-requests%s. Žádosti samotné tam však nepatří." #: html/home.php msgid "Submitting Packages" -msgstr "Odeslání balíčků" +msgstr "Tvorba balíčků" #: html/home.php #, php-format @@ -299,11 +302,11 @@ msgid "" "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." -msgstr "" +msgstr "Pro odesílání balíčků do repozitáře AUR se nyní používá git přes SSH. Další informace naleznete na wiki v sekci %so přidávání balíčků%s na stránce o uživatelském repozitáři systému Arch Linux." #: html/home.php msgid "The following SSH fingerprints are used for the AUR:" -msgstr "Pro AUR se používají následující otisky SSH" +msgstr "Pro repozitář AUR se používají následující otisky SSH:" #: html/home.php msgid "Discussion" @@ -315,7 +318,7 @@ 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 "" +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." #: html/home.php msgid "Bug Reporting" @@ -328,7 +331,7 @@ msgid "" "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 "" +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ář." #: html/home.php msgid "Package Search" @@ -336,11 +339,11 @@ msgstr "Vyhledávání balíčků" #: html/index.php msgid "Adopt" -msgstr "Adoptovat" +msgstr "Převzít balíček" #: html/index.php msgid "Vote" -msgstr "Hlasovat" +msgstr "Dát hlas balíčku" #: html/index.php msgid "UnVote" @@ -348,11 +351,11 @@ msgstr "Odebrat hlas" #: html/index.php template/pkg_search_form.php template/pkg_search_results.php msgid "Notify" -msgstr "Oznamovat" +msgstr "Sledovat" #: html/index.php template/pkg_search_results.php msgid "UnNotify" -msgstr "Neoznamovat" +msgstr "Nesledovat" #: html/index.php msgid "UnFlag" @@ -360,7 +363,7 @@ msgstr "Odznačit" #: html/login.php template/header.php msgid "Login" -msgstr "Přihlásit" +msgstr "Přihlášení" #: html/login.php html/tos.php #, php-format @@ -373,11 +376,11 @@ msgstr "Odhlásit" #: html/login.php msgid "Enter login credentials" -msgstr "Vložit přihlašovací údaje" +msgstr "Zadejte přihlašovací údaje:" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Uživatelské jméno nebo hlavní emailová adresa" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -385,7 +388,7 @@ msgstr "Heslo" #: html/login.php msgid "Remember me" -msgstr "Pamatuj si mě" +msgstr "Zapamatovat přihlášení" #: html/login.php msgid "Forgot Password" @@ -408,7 +411,7 @@ msgstr "Balíčky" #: html/packages.php msgid "Error trying to retrieve package details." -msgstr "Došlo k chybě při získávání detailů balíčku." +msgstr "Při získávání informací o balíčku došlo k chybě." #: html/passreset.php lib/acctfuncs.inc.php msgid "Missing a required field." @@ -416,7 +419,7 @@ msgstr "Chybí povinný údaj." #: html/passreset.php lib/acctfuncs.inc.php msgid "Password fields do not match." -msgstr "Hesla se neshodují" +msgstr "Hesla se neshodují." #: html/passreset.php lib/acctfuncs.inc.php #, php-format @@ -441,7 +444,7 @@ msgstr "Heslo bylo úspěšně resetováno." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Potvrďte uživatelské jméno nebo hlavní emailovou adresu:" #: html/passreset.php msgid "Enter your new password:" @@ -460,21 +463,27 @@ msgstr "Pokračovat" 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 "Pokud jste zapomněli, jaké uživatelské jméno nebo hlavní emailovou adresu jste při registraci použili, pošlete nám zprávu do poštovní konference %saur-general%s." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Zadejte uživatelské jméno nebo hlavní emailovou adresu:" #: html/pkgbase.php msgid "Package Bases" -msgstr "" +msgstr "Základní balíčky" #: html/pkgbase.php msgid "" "The selected packages have not been disowned, check the confirmation " "checkbox." -msgstr "" +msgstr "Zřeknutí se daných balíčků nebylo provedeno. Zřeknutí se je nutné potvrdit zaškrtnutím políčka." + +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "Dané balíčky nebyly převzaty. Převzetí je nutné potvrdit zaškrtnutím políčka." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -482,13 +491,13 @@ msgstr "Nelze najít balíček pro sloučení hlasů a komentářů." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot merge a package base with itself." -msgstr "" +msgstr "Základní balíček není možné sloučit do sebe." #: html/pkgbase.php msgid "" "The selected packages have not been deleted, check the confirmation " "checkbox." -msgstr "" +msgstr "Dané balíčky nebyly smazány. Smazání je nutné potvrdit zaškrtnutím políčka." #: html/pkgdel.php msgid "Package Deletion" @@ -503,7 +512,7 @@ msgstr "Smazat balíček" msgid "" "Use this form to delete the package base %s%s%s and the following packages " "from the AUR: " -msgstr "" +msgstr "Pomocí tohoto formuláře je možné z repozitáře AUR odstranit základní balíček %s%s%s a tyto související balíčky:" #: html/pkgdel.php msgid "Deletion of a package is permanent. " @@ -523,76 +532,84 @@ msgstr "Smazat" #: html/pkgdel.php msgid "Only Trusted Users and Developers can delete packages." -msgstr "" +msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou mazat balíčky." #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" -msgstr "" +msgstr "Zřeknout se balíčku" #: html/pkgdisown.php #, php-format msgid "" "Use this form to disown the package base %s%s%s which includes the following" " packages: " -msgstr "" +msgstr "Pomocí tohoto formuláře je možné zřeknout se základního balíčku %s%s%s, který obsahuje tyto další balíčky:" #: html/pkgdisown.php msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "Zaškrtnutím potvrzujete, že již nadále nechte být spolusprávcem balíčku." #: html/pkgdisown.php #, php-format msgid "" "By selecting the checkbox, you confirm that you want to disown the package " "and transfer ownership to %s%s%s." -msgstr "" +msgstr "Zaškrtnutím potvrzujete, že se chcete zřeknout balíčku a předat ho uživateli %s%s%s." #: html/pkgdisown.php msgid "" "By selecting the checkbox, you confirm that you want to disown the package." -msgstr "" +msgstr "Zaškrtnutím potvrzujete, že se chcete zřeknout balíčku." #: html/pkgdisown.php msgid "Confirm to disown the package" -msgstr "" +msgstr "Potvrzuji, že se zříkám balíčku" #: html/pkgdisown.php msgid "Disown" -msgstr "" +msgstr "Odebrat vlastnictví" #: html/pkgdisown.php msgid "Only Trusted Users and Developers can disown packages." -msgstr "" +msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou odebrat vlastnictví balíčku." #: html/pkgflagcomment.php msgid "Flag Comment" -msgstr "" +msgstr "Označit komentář za nevhodný" #: html/pkgflag.php msgid "Flag Package Out-Of-Date" -msgstr "" +msgstr "Označit balíček za neaktuální" + +#: 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 "Tento balíček je pravděpodobně typu VCS. Prosíme, %sneoznačujte ho%s za neaktuální jen kvůli tomu, že verze balíčku v repozitáři AUR neodpovídá nejnovějšímu commitu. Balíček by měl být označen za neaktuální pouze pokud došlo ke změně umístění zdroje nebo pokud je následkem změn v upstreamu nutná úprava souboru PKGBUILD." #: html/pkgflag.php #, php-format msgid "" "Use this form to flag the package base %s%s%s and the following packages " "out-of-date: " -msgstr "" +msgstr "Pomocí tohoto formuláře je možné označit základní balíček %s%s%s a další tyto balíčky za neaktuální:" #: html/pkgflag.php #, php-format msgid "" "Please do %snot%s use this form to report bugs. Use the package comments " "instead." -msgstr "" +msgstr "Tento formulář %sneslouží%s k hlášení chyb. O těch napište komentář k balíčku." #: html/pkgflag.php msgid "" "Enter details on why the package is out-of-date below, preferably including " "links to the release announcement or the new release tarball." -msgstr "" +msgstr "Uveďte, proč si myslíte, že je balíček neaktuální. Nejlépe přijdete i odkaz na oznámení o nové verzi nebo odkaz na zdrojový tarball." #: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php #: template/pkgreq_results.php @@ -605,11 +622,11 @@ msgstr "Označit" #: html/pkgflag.php msgid "Only registered users can flag packages out-of-date." -msgstr "" +msgstr "Pouze registrovaní uživatelé mohou označovat balíčky za neaktuální." #: html/pkgmerge.php msgid "Package Merging" -msgstr "Spojení balíčku" +msgstr "Sloučení balíčku" #: html/pkgmerge.php template/pkgbase_actions.php msgid "Merge Package" @@ -618,19 +635,19 @@ msgstr "Sloučit balíček" #: html/pkgmerge.php #, php-format msgid "Use this form to merge the package base %s%s%s into another package. " -msgstr "" +msgstr "Pomocí tohoto formuláře je možné sloučit základní balíček %s%s%s do jiného balíčku." #: html/pkgmerge.php msgid "The following packages will be deleted: " -msgstr "Následující balíkčy budou smazány:" +msgstr "Následující balíčky budou smazány:" #: html/pkgmerge.php msgid "Once the package has been merged it cannot be reversed. " -msgstr "" +msgstr "Po sloučení již není možné tento úkon vrátit zpět." #: html/pkgmerge.php msgid "Enter the package name you wish to merge the package into. " -msgstr "" +msgstr "Zadejte název balíčku, do kterého chcete tento balíček sloučit." #: html/pkgmerge.php msgid "Merge into:" @@ -646,7 +663,7 @@ msgstr "Spojit" #: html/pkgmerge.php msgid "Only Trusted Users and Developers can merge packages." -msgstr "" +msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou slučovat balíčky." #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -678,29 +695,29 @@ msgstr "Žádosti" #: html/register.php template/header.php msgid "Register" -msgstr "Registrovat" +msgstr "Registrace" #: html/register.php msgid "Use this form to create an account." -msgstr "Použíte tento formulář k založení účtu." +msgstr "Tento formulář použijte k vytvoření účtu." #: html/tos.php msgid "Terms of Service" -msgstr "" +msgstr "Podmínky užívání" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "" +msgstr "Následující dokumenty byly aktualizovány. Pozorně si je prosím přečtěte:" #: html/tos.php #, php-format msgid "revision %d" -msgstr "" +msgstr "revize %d" #: html/tos.php msgid "I accept the terms and conditions above." -msgstr "" +msgstr "Souhlasím s výše uvedenými podmínkami." #: html/tu.php template/account_details.php template/header.php msgid "Trusted User" @@ -708,7 +725,7 @@ msgstr "Důvěryhodný uživatel" #: html/tu.php msgid "Could not retrieve proposal details." -msgstr "Nelze obdržet navrhované detaily." +msgstr "Načtení informací o návrhu se nezdařilo." #: html/tu.php msgid "Voting is closed for this proposal." @@ -716,19 +733,19 @@ msgstr "Toto hlasování již skončilo." #: html/tu.php msgid "Only Trusted Users are allowed to vote." -msgstr "" +msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou hlasovat." #: html/tu.php msgid "You cannot vote in an proposal about you." -msgstr "Nemůžete volit sami pro sebe." +msgstr "Hlasovat pro sebe se nesmí." #: html/tu.php msgid "You've already voted for this proposal." -msgstr "Již jste hlasoval." +msgstr "Pro tento návrh jsi již hlasoval/a." #: html/tu.php msgid "Vote ID not valid." -msgstr "Nesprávné ID hlasování." +msgstr "ID hlasování není platné." #: html/tu.php template/tu_list.php msgid "Current Votes" @@ -750,11 +767,11 @@ msgstr "Registrace účtu byla pro vaši IP adresu zakázána, pravděpodobně k #: lib/acctfuncs.inc.php msgid "Missing User ID" -msgstr "Chybějící Uživateské ID" +msgstr "Chybí ID uživatele" #: lib/acctfuncs.inc.php msgid "The username is invalid." -msgstr "Chybně zadané uživatelské jméno" +msgstr "Uživatelské jméno není platné." #: lib/acctfuncs.inc.php #, php-format @@ -771,15 +788,15 @@ msgstr "Může obsahovat pouze jednu tečku, podtržítko nebo spojovník." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Potvrďte své nové heslo:" #: lib/acctfuncs.inc.php msgid "The email address is invalid." -msgstr "Vadná emailová adresa." +msgstr "Emailová adresa není platná." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Záložní emailová adresa není platná." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -822,15 +839,15 @@ msgstr "Veřejný SSH klíč, %s%s%s, je již použit." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "Chybí vyplněná CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "Platnost CAPTCHA vypršela. Zkuste to znovu." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "Zadaná CAPTCHA není platná." #: lib/acctfuncs.inc.php #, php-format @@ -848,7 +865,7 @@ msgstr "Na vaši e-mailovou adresu byl odeslán klíč pro obnovení hesla." #: lib/acctfuncs.inc.php msgid "Click on the Login link above to use your account." -msgstr "Kliknutím na odkaz Přihlásit se výše použijte svůj účet." +msgstr "Pro použití svého účtu klikněte na výše uvedený odkaz Přihlášení." #: lib/acctfuncs.inc.php #, php-format @@ -864,12 +881,16 @@ msgstr "Účet, %s%s%s, byl úspěšně změněn." msgid "" "The login form is currently disabled for your IP address, probably due to " "sustained spam attacks. Sorry for the inconvenience." -msgstr "Přihlašovací formulář je momentálně pro vaši IP adresu zakázán, pravděpodobně kvůli trvalým spamovým útokům. Omluvám se za nepříjemnost." +msgstr "Přihlášení je momentálně pro vaši IP adresu zakázáno. Důvodem je nejspíše opakované zasílání spamu z této adresy. Za nepříjemnosti se omlouváme." #: lib/acctfuncs.inc.php msgid "Account suspended" msgstr "Účet pozastaven" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "Nemáte oprávnění k pozastavení účtů." + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -892,7 +913,7 @@ msgstr "Neplatná kombinace resetovacího klíče a e-mailu." #: lib/aur.inc.php template/pkg_details.php msgid "None" -msgstr "" +msgstr "Žádný" #: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -901,7 +922,7 @@ msgstr "Zobrazení informací o účtu pro %s" #: lib/aurjson.class.php msgid "Package base ID or package base name missing." -msgstr "" +msgstr "Chybí ID nebo název základního balíčku." #: lib/aurjson.class.php lib/pkgbasefuncs.inc.php msgid "You are not allowed to edit this comment." @@ -921,11 +942,11 @@ msgstr "Komentář byl přidán" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can edit package information." -msgstr "Musíte být přihlášeni, než budete moci upravovat informace o balíčcích." +msgstr "Pro úpravu informací o balíčcích je nutné se přihlásit." #: lib/pkgbasefuncs.inc.php msgid "Missing comment ID." -msgstr "Chybějící ID komentáře." +msgstr "Chybí ID komentáře." #: lib/pkgbasefuncs.inc.php msgid "No more than 5 comments can be pinned." @@ -937,7 +958,7 @@ msgstr "Nemáte oprávnění připnout tento komentář." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to unpin this comment." -msgstr "Nemáte oprávnění k odepnout tento komentář." +msgstr "Nemáte oprávnění odepnout tento komentář." #: lib/pkgbasefuncs.inc.php msgid "Comment has been pinned." @@ -949,39 +970,63 @@ msgstr "Zrušeno připnutí komentáře." #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Error retrieving package details." -msgstr "Došlo k chybě při získávání detailů o balíčku." +msgstr "Načtení informací o balíčku se nezdařilo." #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Package details could not be found." -msgstr "Detailní informace o balíčku nejsou dostupné." +msgstr "Informace o balíčku nebyly nalezeny." + +#: 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 "Nevybrali jste žádné balíčky pro zapnutí oznámení." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "Oznámení pro vybrané balíčky byla zapnuta." + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "Nevybrali jste žádné balíčky pro vypnutí oznámení." + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "Vybraný balíček nemá zapnutá oznámení." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." +msgstr "Oznámení pro vybrané balíčky byla vypnuta." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." -msgstr "Před nastavením příznaku balíčků se musíte přihlásit." +msgstr "Před označením balíčku se musíte přihlásit." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to flag." -msgstr "Nezvolili jste žádný balíček k označení." +msgstr "Nebyly vybrány žádné balíčky k označení." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have not been flagged, please enter a comment." -msgstr "" +msgstr "Vybrané balíčky nebyly označeny. Zadejte komentář." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been flagged out-of-date." -msgstr "Zvoleným balíčkům byl nastaven příznak zastaralé." +msgstr "Vybrané balíčky byly označeny za neaktuální." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can unflag packages." -msgstr "Musíte být příhlášeni, abyste mohli odznačit balíčky." +msgstr "Před zrušením označení se musíte přihlásit." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to unflag." -msgstr "Nevybrali jste žádne balíčky k odebrání příznaku." +msgstr "Nevybrali jste žádné balíčky, jejichž označení chcete zrušit." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been unflagged." -msgstr "Zvoleným bálíčkům bylo odebráno označení." +msgstr "Označení vybraných balíčků bylo zrušeno." #: lib/pkgbasefuncs.inc.php msgid "You do not have permission to delete packages." @@ -989,69 +1034,81 @@ msgstr "Nemáte oprávnění k odstranění balíčků." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to delete." -msgstr "Nevybrali jste žádné balíčky ke smazání." +msgstr "Nebyly vybrány žádné balíčky ke smazání." + +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Některý z vybraných balíčků neexistuje." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." -msgstr "Zvolené balíčky byly smazány." +msgstr "Vybrané balíčky byly smazány." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can adopt packages." -msgstr "Musíte být přihlášeni, než si budete moci osvojit balíčky." +msgstr "Pro převzetí balíčku je nutné se přihlásit." + +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "Nemáte oprávnění převzít některý z vybraných balíčků." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." -msgstr "Musíte být přihlášeni, pro odebrání vlastnictví u svých balíčků." +msgstr "Pro odebrání vlastnictví balíčku je nutné se přihlásit." + +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "Nemáte oprávnění zřeknout se některého z vybraných balíčků." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." -msgstr "Nevybrali jste žádný balíček k osvojení." +msgstr "Nevybrali jste žádný balíček k převzetí." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to disown." -msgstr "Nevybrali jste žádný balíček pro odebrání vlastnictví." +msgstr "Nebyl vybrán žádný balíček k odebrání vlastnictví." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been adopted." -msgstr "Zvolené balíčky byly osvojeny." +msgstr "Vybrané balíčky byly převzaty." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been disowned." -msgstr "Vybraným balíčkům bylo odebráno vlastnictví." +msgstr "Vlastnictví vybraných balíčků bylo odebráno." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can vote for packages." -msgstr "Před hlasováním se musíte přihlásit." +msgstr "Pro hlasování pro balíček je nutné se přihlásit." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can un-vote for packages." -msgstr "Musíte být přihlášeni, než budete moci odebrat hlasování pro balíčky." +msgstr "Pro odebrání hlasu balíčku je nutné se přihlásit." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to vote for." -msgstr "Nezvolili jste žádné balíčky k hlasování." +msgstr "Nebyly vybrány žádné balíčky pro hlasování." #: lib/pkgbasefuncs.inc.php msgid "Your votes have been removed from the selected packages." -msgstr "Vaše hlasování bylo odebráno vybraným balíčkům." +msgstr "Vybraným balíčkům byl odebrán hlas." #: lib/pkgbasefuncs.inc.php msgid "Your votes have been cast for the selected packages." -msgstr "Vaše hlasování bylo započteno pro vybrané balíčky." +msgstr "Vybraným balíčkům byl přidán hlas." #: lib/pkgbasefuncs.inc.php msgid "Couldn't add to notification list." -msgstr "Nelze přidat do seznamu upozornění." +msgstr "Nelze přidat do seznamu uživatelů k obeznámení." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been added to the comment notification list for %s." -msgstr "Byly jste přidáni do seznamu oznámení ohledně %s." +msgstr "Byly jste přidáni do seznamu uživatelů k obeznámení o balíčku %s." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been removed from the comment notification list for %s." -msgstr "Byli jste odebrání ze seznamu upozornění ohledně %s." +msgstr "Byli jste odebráni ze seznamu uživatelů k obeznámení o balíčku %s." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to undelete this comment." @@ -1063,7 +1120,7 @@ msgstr "Komentář byl obnoven." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to delete this comment." -msgstr "Nemáte oprávnění pro smazání tohoto komentáře." +msgstr "Pro smazání tohoto komentáře nemáte oprávnění." #: lib/pkgbasefuncs.inc.php msgid "Comment has been deleted." @@ -1079,11 +1136,11 @@ msgstr "Nemáte oprávnění upravit klíčová slova tohoto balíčku." #: lib/pkgbasefuncs.inc.php msgid "The package base keywords have been updated." -msgstr "" +msgstr "Klíčová slova základního balíčku byla aktualizována." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to manage co-maintainers of this package base." -msgstr "" +msgstr "Nemáte oprávnění spravovat spolusprávce tohoto balíčku." #: lib/pkgbasefuncs.inc.php #, php-format @@ -1092,7 +1149,7 @@ msgstr "Neplatné uživatelské jméno: %s" #: lib/pkgbasefuncs.inc.php msgid "The package base co-maintainers have been updated." -msgstr "" +msgstr "Spolusprávci základního balíčku byli aktualizováni." #: lib/pkgfuncs.inc.php template/pkgbase_details.php msgid "View packages details for" @@ -1105,11 +1162,11 @@ msgstr "vyžaduje %s" #: lib/pkgreqfuncs.inc.php msgid "You must be logged in to file package requests." -msgstr "" +msgstr "Před podáním žádosti se musíte přihlásit." #: lib/pkgreqfuncs.inc.php msgid "Invalid name: only lowercase letters are allowed." -msgstr "Chybný název: jsou povolena pouze malá písmena." +msgstr "Neplatný název: jsou povolena pouze malá písmena." #: lib/pkgreqfuncs.inc.php msgid "The comment field must not be empty." @@ -1129,7 +1186,7 @@ msgstr "Neplatný důvod." #: lib/pkgreqfuncs.inc.php msgid "Only TUs and developers can close requests." -msgstr "" +msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou zavírat žádosti." #: lib/pkgreqfuncs.inc.php msgid "Request closed successfully." @@ -1138,7 +1195,7 @@ msgstr "Žádost byla úspěšně uzavřena." #: template/account_delete.php #, php-format msgid "You can use this form to permanently delete the AUR account %s." -msgstr "Tento formulář můžete použít k trvalému odstranění účtu AUR %s." +msgstr "Tento formulář můžete použít k trvalému odstranění účtu repozitáře AUR %s." #: template/account_delete.php #, php-format @@ -1195,7 +1252,7 @@ msgstr "Domovská stránka" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php msgid "IRC Nick" -msgstr "IRC přezdívka" +msgstr "Přezdívka na IRC" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php @@ -1205,7 +1262,7 @@ msgstr "Otisk klíče PGP" #: 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" @@ -1243,36 +1300,36 @@ msgstr "Upravit tento uživatelský účet" #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Zobrazit komentáře tohoto uživatele" #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." -msgstr "" +msgstr "Pokud chcete trvale smazat tento účet, klikněte %szde%s." #: template/account_edit_form.php #, php-format msgid "Click %shere%s for user details." -msgstr "" +msgstr "Podrobnosti o uživateli naleznete %szde%s." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Komentáře tohoto účtu naleznete %szde%s." #: template/account_edit_form.php msgid "required" -msgstr "vyžadováno" +msgstr "povinný údaj" #: template/account_edit_form.php msgid "" "Your user name is the name you will use to login. It is visible to the " "general public, even if your account is inactive." -msgstr "" +msgstr "Vaše uživatelské jméno je vaše přihlašovací jméno. Je veřejně viditelné i v případě neaktivního účtu." #: template/account_edit_form.php template/search_accounts_form.php msgid "Normal user" -msgstr "Obyčejný uživatel" +msgstr "Běžný uživatel" #: template/account_edit_form.php template/search_accounts_form.php msgid "Trusted user" @@ -1301,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 "Pokud svou emailovou adresu neskryjete, bude viditelná všem registrovaným uživatelům repozitáře AUR. Skrytá emailová adresa je viditelná pouze správcům systému Arch Linux." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Záložní emailová adresa" #: 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 "Volitelná záložní adresa, pomocí které je možné obnovit přístup k účtu do repozitáře AUR i bez přístupu k hlavní emailové adrese." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Odkazy na obnovení hesla jsou vždy zasílány na hlavní i záložní emailovou adresu." #: 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 "Záložní emailová adresa je vždy viditelná jen správcům systému Arch Linux, bez ohledu na nastavení %s." #: template/account_edit_form.php msgid "Language" @@ -1338,7 +1395,7 @@ msgstr "Časové pásmo" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Pokud chcete své heslo změnit, zadejte nové heslo a pak nové heslo ještě jednou pro potvrzení." #: template/account_edit_form.php msgid "Re-type password" @@ -1348,7 +1405,7 @@ msgstr "Heslo znovu" msgid "" "The following information is only required if you want to submit packages to" " the Arch User Repository." -msgstr "Následující informace jsou požadovány pouze v případě, že chcete odeslat balíčky do 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." #: template/account_edit_form.php msgid "SSH Public Key" @@ -1356,7 +1413,7 @@ msgstr "Veřejný SSH klíč" #: template/account_edit_form.php msgid "Notification settings" -msgstr "Nastavení upozornění" +msgstr "Nastavení oznámení" #: template/account_edit_form.php msgid "Notify of new comments" @@ -1364,29 +1421,29 @@ msgstr "Oznamovat nové komentáře" #: template/account_edit_form.php msgid "Notify of package updates" -msgstr "Oznámení o aktualizacích balíčku" +msgstr "Oznamovat aktualizace balíčků" #: template/account_edit_form.php msgid "Notify of ownership changes" -msgstr "Oznámit změnu vlastnictví" +msgstr "Oznamovat změny vlastnictví" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Pro potvrzení změn profilu zadejte své aktuální heslo:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Vaše aktuální heslo:" #: 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 "Abychom zamezili automatizované tvorbě účtů v repozitáři AUR, laskavě žádáme uživatele, aby zadali výstup z následujícího příkazu:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Odpovědět" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1403,7 +1460,7 @@ msgstr "Reset" #: template/account_search_results.php msgid "No results matched your search criteria." -msgstr "Pro Váš dotaz nebyl nalezen žádný odpovídající výsledek." +msgstr "Zadaným kritériím neodpovídají žádné výsledky." #: template/account_search_results.php msgid "Edit Account" @@ -1433,7 +1490,7 @@ msgstr "Žádné další výsledky." #, php-format msgid "" "Use this form to add co-maintainers for %s%s%s (one user name per line):" -msgstr "" +msgstr "Pomocí tohoto formuláře je možné přidat spolusprávce pro %s%s%s (jedno uživatelské jméno na řádek):" #: template/comaintainers_form.php msgid "Users" @@ -1446,21 +1503,21 @@ msgstr "Uložit" #: template/flag_comment.php #, php-format msgid "Flagged Out-of-Date Comment: %s" -msgstr "" +msgstr "Komentář k označení za neaktuální: %s" #: template/flag_comment.php #, php-format msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" -msgstr "" +msgstr "Uživatel %s%s%s označil balíček %s%s%s za neaktuální dne %s%s%s z následujícího důvodu:" #: template/flag_comment.php #, php-format msgid "%s%s%s is not flagged out-of-date." -msgstr "" +msgstr "Balíček %s%s%s nebyl označen za neaktuální." #: template/flag_comment.php msgid "Return to Details" -msgstr "" +msgstr "Zpět na podrobnosti balíčku" #: template/footer.php #, php-format @@ -1473,7 +1530,7 @@ msgstr "Můj účet" #: template/pkgbase_actions.php msgid "Package Actions" -msgstr "Dostupné akce pro balíček" +msgstr "Akce balíčku" #: template/pkgbase_actions.php msgid "View PKGBUILD" @@ -1494,11 +1551,11 @@ msgstr "Prohledat wiki" #: template/pkgbase_actions.php #, php-format msgid "Flagged out-of-date (%s)" -msgstr "" +msgstr "Označeno za neaktuální (%s)" #: template/pkgbase_actions.php msgid "Flag package out-of-date" -msgstr "Označit balíček jako zastaralý" +msgstr "Označit balíček za neaktuální" #: template/pkgbase_actions.php msgid "Unflag package" @@ -1522,28 +1579,28 @@ msgstr "Zapnout oznámení" #: template/pkgbase_actions.php msgid "Manage Co-Maintainers" -msgstr "" +msgstr "Spravovat spolusprávce" #: template/pkgbase_actions.php #, php-format msgid "%d pending request" msgid_plural "%d pending requests" -msgstr[0] "%d čekající požadavek" +msgstr[0] "%d žádost na vyřízení" msgstr[1] "%d čekající požadavky" -msgstr[2] "%d čekajících žádostí" -msgstr[3] "%d čekajících žádostí" +msgstr[2] "%d žádostí na vyřízení" +msgstr[3] "%d žádostí na vyřízení" #: template/pkgbase_actions.php msgid "Adopt Package" -msgstr "Adoptovat balíček" +msgstr "Převzít balíček" #: template/pkgbase_details.php msgid "Package Base Details" -msgstr "" +msgstr "Informace o základním balíčku" #: template/pkgbase_details.php template/pkg_details.php msgid "Git Clone URL" -msgstr "" +msgstr "URL pro příkaz git clone" #: template/pkgbase_details.php template/pkg_details.php msgid "read-only" @@ -1551,7 +1608,7 @@ msgstr "jen pro čtení" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "kliknutím zkopírujete" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1561,7 +1618,7 @@ msgstr "Klíčová slova" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Submitter" -msgstr "" +msgstr "Vytvořil" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php @@ -1570,7 +1627,7 @@ msgstr "Správce" #: template/pkgbase_details.php template/pkg_details.php msgid "Last Packager" -msgstr "" +msgstr "Naposledy zabalil" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php @@ -1584,7 +1641,7 @@ msgstr "Popularita" #: template/pkgbase_details.php template/pkg_details.php msgid "First Submitted" -msgstr "Poprvé vložen" +msgstr "Poprvé vytvořil" #: template/pkgbase_details.php template/pkg_details.php msgid "Last Updated" @@ -1593,7 +1650,7 @@ msgstr "Naposledy aktualizováno" #: template/pkg_comment_box.php #, php-format msgid "Edit comment for: %s" -msgstr "" +msgstr "Upravit komentář pro: %s" #: template/pkg_comment_box.php template/pkg_comment_form.php msgid "Add Comment" @@ -1603,12 +1660,12 @@ msgstr "Přidat komentář" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Identifikátory git commitů v repozitáři AUR a URL jsou automaticky převáděny na klikatelné odkazy." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "Značkovací %ssyntaxe Markdown%s je částěčně podporována." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1620,7 +1677,7 @@ msgstr "Nejnovější komentáře" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Komentáře uživatele" #: template/pkg_comments.php #, php-format @@ -1630,22 +1687,22 @@ msgstr "%s přidal komentář %s" #: template/pkg_comments.php #, php-format msgid "Anonymous comment on %s" -msgstr "" +msgstr "Anonymní komentář dne %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Okomentoval balíček %s dne %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" -msgstr "" +msgstr "smazáno dne %s uživatelem %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s" -msgstr "" +msgstr "smazáno dne %s" #: template/pkg_comments.php #, php-format @@ -1659,7 +1716,7 @@ msgstr "upraveno %s" #: template/pkg_comments.php msgid "Undelete comment" -msgstr "" +msgstr "Zrušit smazání komentáře" #: template/pkg_comments.php msgid "Delete comment" @@ -1675,11 +1732,11 @@ msgstr "Odepnout komentář" #: template/pkg_details.php msgid "Package Details" -msgstr "Detaily balíčku" +msgstr "Informace o balíčku" #: template/pkg_details.php template/pkg_search_form.php msgid "Package Base" -msgstr "" +msgstr "Základní balíček" #: template/pkg_details.php template/pkg_search_results.php msgid "Description" @@ -1687,11 +1744,11 @@ msgstr "Popis" #: template/pkg_details.php msgid "Upstream URL" -msgstr "" +msgstr "URL pro upstream" #: template/pkg_details.php msgid "Visit the website for" -msgstr "" +msgstr "Navštívit web pro" #: template/pkg_details.php msgid "Licenses" @@ -1715,11 +1772,11 @@ msgstr "Nahrazuje" #: template/pkg_details.php msgid "Dependencies" -msgstr "Závislosti" +msgstr "Vyžadované balíčky" #: template/pkg_details.php msgid "Required by" -msgstr "Vyžadováno" +msgstr "Vyžadováno balíčky" #: template/pkg_details.php msgid "Sources" @@ -1728,13 +1785,13 @@ msgstr "Zdroje" #: template/pkgreq_close_form.php #, php-format msgid "Use this form to close the request for package base %s%s%s." -msgstr "" +msgstr "Pomocí tohoto formuláře je možné zavřít požadavek pro základní balíček %s%s%s." #: template/pkgreq_close_form.php msgid "" "The comments field can be left empty. However, it is highly recommended to " "add a comment when rejecting a request." -msgstr "" +msgstr "Pole pro komentáře může být prázdné. Silně se však doporučuje nějaký komentář přidat, pokud požadavek odmítáte." #: template/pkgreq_close_form.php msgid "Reason" @@ -1755,7 +1812,7 @@ msgstr "Zamítnuto" msgid "" "Use this form to file a request against package base %s%s%s which includes " "the following packages:" -msgstr "" +msgstr "Pomocí tohoto formuláře je možné vytvořit požadavek pro základní balíček %s%s%s, který obsahuje tyto další balíčky:" #: template/pkgreq_form.php msgid "Request type" @@ -1767,18 +1824,18 @@ msgstr "Smazání" #: template/pkgreq_form.php msgid "Orphan" -msgstr "Sirotek" +msgstr "Odebrání vlastnictví" #: template/pkgreq_form.php template/pkg_search_results.php msgid "Merge into" -msgstr "Sloučit do" +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 " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "" +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ů." #: template/pkgreq_form.php msgid "" @@ -1786,7 +1843,7 @@ msgid "" "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 "" +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." #: template/pkgreq_form.php msgid "" @@ -1794,7 +1851,7 @@ msgid "" "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 "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." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1804,10 +1861,10 @@ msgstr "Žádné žádosti neodpovídaly vašim kritériím vyhledávání." #, php-format msgid "%d package request found." msgid_plural "%d package requests found." -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" +msgstr[0] "Byl nalezen %d požadavek na balíček." +msgstr[1] "Byly nalezeny %d požadavky na balíček." +msgstr[2] "Bylo nalezeno %d požadavků na balíček." +msgstr[3] "Bylo nalezeno %d požadavků na balíček." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1820,7 +1877,7 @@ msgstr "Balíček" #: template/pkgreq_results.php msgid "Filed by" -msgstr "" +msgstr "Zadal" #: template/pkgreq_results.php msgid "Date" @@ -1830,23 +1887,23 @@ msgstr "Datum" #, php-format msgid "~%d day left" msgid_plural "~%d days left" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" +msgstr[0] "Zbývá asi %dden" +msgstr[1] "Zbývají asi %d dny" +msgstr[2] "Zbývá asi %d dní" +msgstr[3] "Zbývá asi %d dní" #: template/pkgreq_results.php #, php-format msgid "~%d hour left" msgid_plural "~%d hours left" -msgstr[0] "" -msgstr[1] "" -msgstr[2] "" -msgstr[3] "" +msgstr[0] "Zbývá asi %d hodina" +msgstr[1] "Zbývají asi %d hodiny" +msgstr[2] "Zbývá asi %d hodin" +msgstr[3] "Zbývá asi %d hodin" #: template/pkgreq_results.php msgid "<1 hour left" -msgstr "" +msgstr "Zbývá méně než 1 hodina" #: template/pkgreq_results.php msgid "Accept" @@ -1862,7 +1919,7 @@ msgstr "Uzavřít" #: template/pkgreq_results.php msgid "Pending" -msgstr "" +msgstr "Čeká na vyřízení" #: template/pkgreq_results.php msgid "Closed" @@ -1882,15 +1939,15 @@ msgstr "Přesné jméno" #: template/pkg_search_form.php msgid "Exact Package Base" -msgstr "" +msgstr "Přesný název základního balíčku" #: template/pkg_search_form.php msgid "Co-maintainer" -msgstr "" +msgstr "Spolusprávce" #: template/pkg_search_form.php msgid "Maintainer, Co-maintainer" -msgstr "" +msgstr "Správce, spolusprávce" #: template/pkg_search_form.php msgid "All" @@ -1911,7 +1968,7 @@ msgstr "Název" #: template/pkg_search_form.php template/pkg_search_results.php #: template/tu_details.php template/tu_list.php msgid "Voted" -msgstr "Hlasováno" +msgstr "Hlasoval/a jsem" #: template/pkg_search_form.php msgid "Last modified" @@ -1935,11 +1992,11 @@ msgstr "Vyhledat dle" #: template/pkg_search_form.php template/stats/user_table.php msgid "Out of Date" -msgstr "Zastaralé" +msgstr "Neaktuální" #: template/pkg_search_form.php template/search_accounts_form.php msgid "Sort by" -msgstr "Seřadit dle" +msgstr "Řadit dle" #: template/pkg_search_form.php msgid "Sort order" @@ -1947,23 +2004,23 @@ msgstr "Řadit" #: template/pkg_search_form.php msgid "Per page" -msgstr "Na jedné stránce" +msgstr "Výsledků na stránku" #: template/pkg_search_form.php template/pkg_search_results.php msgid "Go" -msgstr "Jdi" +msgstr "Hledat" #: template/pkg_search_form.php msgid "Orphans" -msgstr "Sirotci" +msgstr "Bez vlastníka" #: template/pkg_search_results.php msgid "Error retrieving package list." -msgstr "Chyba při získávání seznamu balíčků." +msgstr "Načtení seznamu balíčků se nezdařilo." #: template/pkg_search_results.php msgid "No packages matched your search criteria." -msgstr "Žádný balíček neodpovídá zadaným kritériím." +msgstr "Zadaným kritériím neodpovídá žádný balíček." #: template/pkg_search_results.php #, php-format @@ -1992,7 +2049,7 @@ msgstr "Ano" #: template/pkg_search_results.php msgid "orphan" -msgstr "sirotek" +msgstr "bez vlastníka" #: template/pkg_search_results.php msgid "Actions" @@ -2000,15 +2057,15 @@ msgstr "Akce" #: template/pkg_search_results.php msgid "Unflag Out-of-date" -msgstr "Odebrat příznak zastaralý" +msgstr "Zrušit označení za neaktuální" #: template/pkg_search_results.php msgid "Adopt Packages" -msgstr "Osvojit balíček" +msgstr "Převzít balíčky" #: template/pkg_search_results.php msgid "Disown Packages" -msgstr "Odebrat vlastnictví" +msgstr "Zřeknout se balíčků" #: template/pkg_search_results.php msgid "Delete Packages" @@ -2032,31 +2089,31 @@ msgstr "Statistiky" #: template/stats/general_stats_table.php msgid "Orphan Packages" -msgstr "Balíčků bez správce" +msgstr "Bez vlastníka" #: template/stats/general_stats_table.php msgid "Packages added in the past 7 days" -msgstr "Přidaných balíčků za posledních 7 dní" +msgstr "Přidáno (poslední týden)" #: template/stats/general_stats_table.php msgid "Packages updated in the past 7 days" -msgstr "Aktualizovaných balíčků za posledních 7 dní" +msgstr "Aktualizováno (poslední týden)" #: template/stats/general_stats_table.php msgid "Packages updated in the past year" -msgstr "Aktualizovaných balíčků v posledním roce" +msgstr "Aktualizováno (poslední rok)" #: template/stats/general_stats_table.php msgid "Packages never updated" -msgstr "Balíčků, které nebyly nikdy aktualizovány" +msgstr "Nikdy neaktualizováno" #: template/stats/general_stats_table.php msgid "Registered Users" -msgstr "Registrovaných uživatelů" +msgstr "Registrovaní uživatelé" #: template/stats/general_stats_table.php msgid "Trusted Users" -msgstr "Důvěryhodných uživatelů" +msgstr "Důvěryhodní uživatelé" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2072,7 +2129,7 @@ msgstr "Moje statistiky" #: template/tu_details.php msgid "Proposal Details" -msgstr "Detaily návrhu" +msgstr "Informace o návrhu" #: template/tu_details.php msgid "This vote is still running." @@ -2081,7 +2138,7 @@ msgstr "Hlasování stále probíhá." #: template/tu_details.php #, php-format msgid "Submitted: %s by %s" -msgstr "Vloženo: %s od %s" +msgstr "Vytvořeno: %s od %s" #: template/tu_details.php template/tu_list.php msgid "End" @@ -2109,7 +2166,7 @@ msgstr "Účast" #: template/tu_last_votes_list.php msgid "Last Votes by TU" -msgstr "" +msgstr "Poslední hlasy důvěryhodného uživatele" #: template/tu_last_votes_list.php msgid "Last vote" @@ -2117,7 +2174,7 @@ msgstr "Naposledy hlasováno" #: template/tu_last_votes_list.php template/tu_list.php msgid "No results found." -msgstr "Žádné výsledky." +msgstr "Nebyly nalezeny žádné výsledky." #: template/tu_list.php msgid "Start" @@ -2129,7 +2186,7 @@ msgstr "Zpět" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "" +msgstr "Obnovení hesla k účtu repozitáře AUR" #: scripts/notify.py #, python-brace-format @@ -2137,90 +2194,90 @@ 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 "Obdrželi jsme žádost o obnovení hesla k účtu {user}, který je spojen s touto emailovou adresou. Pokud chcete heslo obnovit, použijte níže uvedený odkaz [1]. V opačném případě tuto zprávu ignorujte a ke žádné změně nedojde." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "Vítejte v repozitáři AUR, uživatelském repozitáři systému Arch Linux" #: 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ítejte v uživatelském repozitáři systému Arch Linux! Pro nastavení počátečního heslo nového účtu použijte níže uvedený odkaz [1]. Pokud odkaz nefunguje, zkuste ho zkopírovat a vložit prohlížeče." #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "Komentář na webu AUR u balíčku {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "Uživatel {user} [1] přidal k balíčku {pkgbase} [2] následující komentář:" #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Pokud již nechcete dostávat oznámení související s tímto balíčkem, na stránce balíčku [2] vyberte možnost \"{label}\"." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "Aktualizace balíčku na webu AUR: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "Uživatel {user} [1] provedl aktualizaci balíčku {pkgbase} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "Oznámení o neaktuálním stavu balíčku {pkgbase} na webu AUR" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "Váš balíček {pkgbase} [1] byl uživatel {user} [2] označen za neaktuální:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "Oznámení o změně vlastnictví balíčku {pkgbase} na webu AUR" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "Balíček {pkgbase} [1] byl převzat uživatelem {user} [2]." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "Uživatel {user} [2] se zřekl balíčku {pkgbase} [1] a tento balíček je tak nyní bez vlastníka." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "Oznámení o spolusprávě balíčku {pkgbase} na webu AUR" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Byli jste přidáni jako spolusprávce balíčku {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Byli jste odebráni ze seznamu spolusprávců balíčku {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "Smazání balíčku {pkgbase} na webu AUR" #: scripts/notify.py #, python-brace-format @@ -2229,7 +2286,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "Uživatel {user} [1] sloučil balíček {old} [2] do balíčku {new} [3].\n\n-- \nPokud již nechcete dostávat oznámení související s tímto balíčkem, na stránce balíčku [3] vyberte možnost \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2237,16 +2294,61 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "Uživatel {user} [1] smazal balíček {pkgbase} [2].\n\nNadále již nebude dostávat oznámení související s tímto balíčkem." #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "Připomínka k hlasování důvěryhodných uživatelů: návrh {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 "Nezapomeňte prosím hlasovat o návrhu {id} [1]. Hlasování končí za méně než 48 hodin." + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Poskytnutý typ účtu je neplatný." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "Nemáte oprávnění ke změnám typů účtů." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +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 "" + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/da.po b/po/da.po index b78fc785..a6f290ea 100644 --- a/po/da.po +++ b/po/da.po @@ -11,8 +11,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -473,6 +473,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 "" @@ -571,6 +577,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 "" @@ -867,6 +881,10 @@ msgstr "" msgid "Account suspended" msgstr "Konto suspenderet" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -952,6 +970,30 @@ msgstr "Fejl ved modtagelse af pakkedetaljer." msgid "Package details could not be found." msgstr "Kunne ikke finde pakkedetaljerne.." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Du skal være logget ind for at markere pakker." @@ -988,6 +1030,10 @@ msgstr "Du har ikke tilladelse til, at slette pakker." msgid "You did not select any packages to delete." msgstr "Du har ikke valgt nogle pakker at slette." +#: 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 "De valgte pakker er blevet slettet." @@ -996,10 +1042,18 @@ msgstr "De valgte pakker er blevet slettet." msgid "You must be logged in before you can adopt packages." msgstr "Du skal være logget ind for at adoptere pakker." +#: 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 "Du skal være logget ind for at opgive ejerskabet for pakker." +#: 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 "Du har ikke valgt nogle pakker at adoptere." @@ -2237,3 +2291,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/de.po b/po/de.po index 8b2ea808..ec0a0fbe 100644 --- a/po/de.po +++ b/po/de.po @@ -13,22 +13,24 @@ # go2sh , 2015 # Lukas Fleischer , 2011 # Mark Gerlach, 2015 -# Mark Gerlach, 2015 +# a980e5e9ed6813c0751cebb84f7948a8_ff57647, 2015 # Matthias Gorissen , 2012 # Nuc1eoN , 2014 # Nuc1eoN , 2014 # Simon Legner , 2018 # Simon Schneider , 2011 -# Stefan Auditor , 2017-2018,2020 +# Stefan Auditor , 2021 +# Stefan Auditor , 2017-2018,2020 # Thomas_Do , 2013-2014 # Thomas_Do , 2012-2013 +# Yunus Kahveci, 2021 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-01-31 11:12+0000\n" -"Last-Translator: Stefan Auditor \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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -69,7 +71,7 @@ msgstr "Dienst nicht verfügbar" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "Keine Panik! Diese Seite ist wegen Verwaltungs-Aufgaben geschlossen. Wir werden gleich zurück sein." +msgstr "Keine Panik! Diese Seite ist wegen Wartungsarbeiten geschlossen. Wir werden gleich zurück sein." #: html/account.php msgid "Account" @@ -283,7 +285,7 @@ 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 leich repariert werden kann. Sondern kontaktiere den Maintainer und reiche eine Verwaisungsanfrage ein, wenn nötig." +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." #: html/home.php msgid "Merge Request" @@ -473,7 +475,7 @@ msgstr "Weiter" 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 "Wenn Du den bei der Registrierung vernwendeten Benutzernamen und primäre E-Mail-Adresse vergessen hast, sende bitte eine Nachricht an die %saur-general%s Mailingliste." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" @@ -489,6 +491,12 @@ msgid "" "checkbox." msgstr "Die Betreuung der ausgewählten Pakete wurde nicht abgegeben, überprüfe die Bestätigungs-Checkbox." +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "Die ausgewählten Pakete wurden nicht übernommen, überprüfe die Bestätigungs-Checkbox." + #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." msgstr "Das Paket, in das die Stimmen und Kommentare übernommen werden sollen, kann nicht gefunden werden." @@ -577,7 +585,7 @@ 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." +msgstr "Nur TUs und Developer können die Paket-Betreuung abgeben." #: html/pkgflagcomment.php msgid "Flag Comment" @@ -587,6 +595,14 @@ msgstr "Kommentar markieren" msgid "Flag Package Out-Of-Date" msgstr "Paket als \"veraltet\" markieren" +#: 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 "Dies ist scheinbar ein VCS Paket. Bitte markiere es %snicht%s als veraltet, wenn die Paket-Version im AUR nicht dem letzten Commit entspricht. Dieses Paket sollte nur dann markiert werden, wenn die Quellen verschoben wurden oder Änderungen aufgrund kürzlicher Änderungen Upstream im PKGBUILD notwendig sind." + #: html/pkgflag.php #, php-format msgid "" @@ -883,6 +899,10 @@ msgstr "Das Loginformular ist aktuell für deine IP-Adresse deaktiviert. Ein mö msgid "Account suspended" msgstr "Konto aufgehoben" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "Du hast keine Berechtigung Kontos zu sperren." + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -968,6 +988,30 @@ msgstr "Fehler beim Aufrufen der Paket-Details." msgid "Package details could not be found." msgstr "Paket-Details konnten nicht gefunden werden." +#: 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 "Du hast keine Pakete ausgewählt über die Du benachrichtigt werden möchtest." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "Die Benachrichtigungen wurden für das ausgewählte Paket aktiviert." + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "Du hast keine Pakete ausgewählt über die Du nicht mehr benachrichtigt werden möchtest." + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "Ein Paket das Du ausgewählt hast hat keine Benachrichtigungen aktiviert." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." +msgstr "Die Benachrichtigungen wurden für das ausgewählte Paket entfernt." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." msgstr "Du musst Dich anmelden, um Pakete markieren zu können." @@ -1004,6 +1048,10 @@ msgstr "Du hast keine Berechtigung zum Löschen von Paketen." msgid "You did not select any packages to delete." msgstr "Du hast keine Pakete zum Löschen ausgewählt." +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Eines der ausgewählten Pakete existiert nicht." + #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." msgstr "Die gewählten Pakete wurden gelöscht." @@ -1012,10 +1060,18 @@ msgstr "Die gewählten Pakete wurden gelöscht." msgid "You must be logged in before you can adopt packages." msgstr "Du musst angemeldet sein, um Pakete übernehmen zu können." +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "Du hast keine Berechtigung eines der ausgewählten Pakete zu übernehmen." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." msgstr "Du musst anmeldet sein, um die Betreuung eines Pakets abzugeben." +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "Du hast keine Berechtigung eines der ausgewählten Pakete abzugeben." + #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." msgstr "Du hast keine Pakete zum Übernehmen gewählt." @@ -1271,7 +1327,7 @@ msgstr "Klicke %shere%s für Benutzerdetails." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Klicke %shier%s um die Kommentare aufzulisten, die dieser Benutzer gemacht hat." #: template/account_edit_form.php msgid "required" @@ -1314,7 +1370,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 "Wenn Du Deine E-Mail-Adresse nicht versteckst, ist sie für alle registrierten AUR Benutzer sichtbar. Wenn Du sie versteckst, ist sie nur für Arch Linux Mitarbeiter sichtbar." #: template/account_edit_form.php msgid "Backup Email Address" @@ -1324,20 +1380,20 @@ msgstr "Sicherungs E-Mail-Adresse" 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 "Gib optional eine sekundäre E-Mail-Adresse an, mit der Du Dein Konto wiederherstellen kannst, falls der Zugriff auf die primäre E-Mail-Adresse verloren geht." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Links zum Zurücksetzen des Passworts werden immer sowohl an die primäre als auch an die Backup-E-Mail-Adresse gesendet." #: 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 "Die Backup-E-Mail-Adresse nur für die Arch Linux Mitarbeiter sichtbar, unabhängig von der %s-Einstellung." #: template/account_edit_form.php msgid "Language" @@ -1351,7 +1407,7 @@ msgstr "Zeitzone" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Willst Du das Passwort ändern, gib ein neues Passwort ein und bestätige das neue Passwort durch erneute Eingabe." #: template/account_edit_form.php msgid "Re-type password" @@ -1395,7 +1451,7 @@ msgstr "Dein aktuelles Passwort" msgid "" "To protect the AUR against automated account creation, we kindly ask you to " "provide the output of the following command:" -msgstr "" +msgstr "Um das AUR gegen automatische Benutzer-Erstellung zu schützen, gib bitte die Ausgabe folgenden Befehls an:" #: template/account_edit_form.php msgid "Answer" @@ -1614,7 +1670,7 @@ msgstr "Kommentar hinzufügen" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Git Commit-Kennungen, die einen Commit in einem AUR Paket referenzieren und URLs werden automatisch in Links umgewandelt." #: template/pkg_comment_form.php #, php-format @@ -1646,7 +1702,7 @@ msgstr "Anonym kommentierte %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Kommentiert auf Paket %s am %s" #: template/pkg_comments.php #, php-format @@ -2198,7 +2254,7 @@ msgstr "AUR Besitzer Benachrichtigung für {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "Das Paket {pkgbase} [1] wurde von {user} [2] adoptiert." +msgstr "Das Paket {pkgbase} [1] wurde von {user} [2] übernommen." #: scripts/notify.py #, python-brace-format @@ -2232,7 +2288,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] führte {old} [2] in {new} [3] zusammen.\n\n-- \nWenn Du Benachrichtigungen über das neue Paket nicht mehr erhalten möchtest, gehe bitte auf [3] und klicke \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2253,3 +2309,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "Bitte denke daran für Vorschlag {id} [1] deine Stimme anzugeben. Die Abstimmungspriode endet in weniger als 48 Stunden." + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Ungültiger Account-Typ bereitgestellt." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "Du hast keine Berechtigung Konto-Typen zu ändern." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +msgstr "Du hast keine Berechtigung, den Konto-Typ dieses Benutzers zu %s zu ändern." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "" + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/el.po b/po/el.po index 7af57544..f1fe704e 100644 --- a/po/el.po +++ b/po/el.po @@ -8,6 +8,7 @@ # Achilleas Pipinellis, 2013 # Achilleas Pipinellis, 2011 # Achilleas Pipinellis, 2012 +# Leonidas Spyropoulos, 2021 # Lukas Fleischer , 2011 # flamelab , 2011 msgid "" @@ -15,8 +16,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -61,7 +62,7 @@ msgstr "" #: html/account.php msgid "Account" -msgstr "" +msgstr "Λογαριασμός" #: html/account.php template/header.php msgid "Accounts" @@ -81,7 +82,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." @@ -235,11 +236,11 @@ msgstr "" #: html/home.php msgid "Learn more..." -msgstr "" +msgstr "Μάθετε περισσότερα..." #: html/home.php msgid "Support" -msgstr "" +msgstr "Υποστήριξη" #: html/home.php msgid "Package Requests" @@ -477,6 +478,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 "Δεν μπορεί να βρεθεί το πακέτο για τη συγχώνευση ψήφων και σχόλιων" @@ -575,6 +582,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 "" @@ -598,7 +613,7 @@ msgstr "" #: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php #: template/pkgreq_results.php msgid "Comments" -msgstr "" +msgstr "Σχόλια" #: html/pkgflag.php msgid "Flag" @@ -651,11 +666,11 @@ msgstr "Μόνο οι Trusted Users και οι Developers μπορούν να #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" -msgstr "" +msgstr "Δημιουργία αιτήματος" #: html/pkgreq.php template/pkgreq_close_form.php msgid "Close Request" -msgstr "" +msgstr "Κλείσιμο αιτήματος" #: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" @@ -675,7 +690,7 @@ msgstr "Τελευταίο" #: html/pkgreq.php template/header.php msgid "Requests" -msgstr "" +msgstr "Αιτήματα" #: html/register.php template/header.php msgid "Register" @@ -687,7 +702,7 @@ msgstr "Χρησιμοποιήστε αυτή τη φόρμα για να δημ #: html/tos.php msgid "Terms of Service" -msgstr "" +msgstr "Όροι χρήσης" #: html/tos.php msgid "" @@ -701,7 +716,7 @@ msgstr "" #: html/tos.php msgid "I accept the terms and conditions above." -msgstr "" +msgstr "Αποδέχομαι τους παραπάνω όρους χρήσης." #: html/tu.php template/account_details.php template/header.php msgid "Trusted User" @@ -772,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." @@ -871,6 +886,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 "" @@ -956,6 +975,30 @@ msgstr "Σφάλμα κατά τη διάρκεια φόρτωσης των πλ msgid "Package details could not be found." msgstr "Οι πληροφορίες του πακέτου δεν μπόρεσαν να βρεθούν." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Πρέπει να έχετε συνδεθεί για να επισημάνετε τα πακέτα." @@ -992,6 +1035,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 "Tα επιλεγμένα πακέτα έχουν διαγραφεί." @@ -1000,10 +1047,18 @@ msgstr "Tα επιλεγμένα πακέτα έχουν διαγραφεί." 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 "Δεν επιλέξατε κανένα πακέτο για να υιοθετήσετε." @@ -2241,3 +2296,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/es.po b/po/es.po index d60f45f1..ea7ac099 100644 --- a/po/es.po +++ b/po/es.po @@ -20,8 +20,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -482,6 +482,12 @@ msgid "" "checkbox." msgstr "Los paquetes seleccionados no se han abandonado, marca la casilla de confirmación." +#: 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 "No se puede encontrar el paquete para unir los votos y comentarios en él." @@ -580,6 +586,14 @@ msgstr "Marcar comentario" msgid "Flag Package Out-Of-Date" msgstr "Marcado como obsoleto" +#: 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 "" @@ -876,6 +890,10 @@ msgstr "El formulario de registro ha sido deshabilitado para tu dirección IP, p msgid "Account suspended" msgstr "Cuenta suspendida" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -961,6 +979,30 @@ msgstr "Error al recuperar los detalles del paquete." msgid "Package details could not be found." msgstr "Los detalles del paquete no se han podido encontrar." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Debes autentificarte antes de poder marcar paquetes." @@ -997,6 +1039,10 @@ msgstr "No posees los permisos para eliminar paquetes." msgid "You did not select any packages to delete." msgstr "No seleccionaste ningún paquete a eliminar." +#: 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 "Los paquetes seleccionados se han eliminado." @@ -1005,10 +1051,18 @@ msgstr "Los paquetes seleccionados se han eliminado." msgid "You must be logged in before you can adopt packages." msgstr "Debes autentificarte antes de poder adoptar paquetes." +#: 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 "Debes autentificarte antes de poder abandonar paquetes." +#: 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 "No haz seleccionado ningún paquete para ser adoptado." @@ -2246,3 +2300,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/et.po b/po/et.po new file mode 100644 index 00000000..9b6493b5 --- /dev/null +++ b/po/et.po @@ -0,0 +1,2335 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: et\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/fi.po b/po/fi.po index d2daad80..39cfe626 100644 --- a/po/fi.po +++ b/po/fi.po @@ -5,15 +5,15 @@ # Translators: # Elias Autio, 2016 # Jesse Jaara , 2011-2012,2015 -# Nikolay Korotkiy , 2018-2019 +# Nikolay Korotkiy , 2018-2019 # Sami Korkalainen, 2018 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -474,6 +474,12 @@ msgid "" "checkbox." msgstr "Seuraavia paketteja ei ole hylätty. Tarkista varmistusruutu." +#: 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 "Pakettia, johon haluat siirtää äänet ja kommentit, ei löydy." @@ -572,6 +578,14 @@ msgstr "" msgid "Flag Package Out-Of-Date" msgstr "Merkitse paketti vanhentuneeksi" +#: 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 "" @@ -868,6 +882,10 @@ msgstr "Kirjautumislomake on estetty IP-osoitteestasi, luultavasti toistuvien ro msgid "Account suspended" msgstr "Tili jäädytetty" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -953,6 +971,30 @@ msgstr "Virhe haettaessa paketin tietoja." msgid "Package details could not be found." msgstr "Paketin tietoja ei löydetty." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Sinun pitää kirjautua, ennen kuin voit muuttaa merkintöjä." @@ -989,6 +1031,10 @@ msgstr "Sinulla ei ole oikeutta poistaa paketteja." msgid "You did not select any packages to delete." msgstr "Et valinnut yhtään pakettia poistettavaksi." +#: 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 "Valitut paketit on nyt poistettu." @@ -997,10 +1043,18 @@ msgstr "Valitut paketit on nyt poistettu." msgid "You must be logged in before you can adopt packages." msgstr "Sinun pitää kirjautua, ennen kuin voit adoptoida paketteja." +#: 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 "Sinun pitää kirjautua, ennen kuin voit hylätä paketteja." +#: 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 "Et valinnut yhtään pakettia adoptoitavaksi." @@ -2238,3 +2292,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/fi_FI.po b/po/fi_FI.po new file mode 100644 index 00000000..f3253433 --- /dev/null +++ b/po/fi_FI.po @@ -0,0 +1,2335 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fi_FI\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/fr.po b/po/fr.po index e73a8c99..99d01460 100644 --- a/po/fr.po +++ b/po/fr.po @@ -7,18 +7,21 @@ # Antoine Lubineau , 2012 # Antoine Lubineau , 2012-2016 # Cedric Girard , 2011,2014,2016 +# demostanis , 2020 +# Kristien , 2020 # lordheavy , 2011 # lordheavy , 2013-2014,2018 # lordheavy , 2011-2012 # Lukas Fleischer , 2011 -# Xorg, 2015,2017,2019 +# Thibault , 2020 +# df3de0cb43d289cd23a753345b3743cd_a20684f, 2015,2017,2019 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -83,7 +86,7 @@ msgstr "Vous n’avez pas la permission d’éditer ce compte." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Mot de passe invalide." #: html/account.php msgid "Use this form to search existing accounts." @@ -380,7 +383,7 @@ msgstr "Entrez vos identifiants" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Nom de l'utilisateur ou adresse email principale" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -444,7 +447,7 @@ msgstr "Votre mot de passe a été réinitialisé avec succès." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Veuillez confirmer votre nom d'utilisateur ou votre adresse email principale:" #: html/passreset.php msgid "Enter your new password:" @@ -463,11 +466,11 @@ msgstr "Continuer" 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 vous avez oublié votre nom d'utilisateur et votre adresse email principale, veuillez s'il vous plaît envoyer un message à %saur-general%s" #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Entrez votre nom d'utilisateur ou votre email principal:" #: html/pkgbase.php msgid "Package Bases" @@ -479,6 +482,12 @@ msgid "" "checkbox." msgstr "Les paquets sélectionnés n'ont pas été destitués, vérifiez la boîte de confirmation." +#: 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 "Impossible de trouver le paquet dans lequel fusionner les votes et les commentaires." @@ -577,6 +586,14 @@ msgstr "Signaler le commentaire" msgid "Flag Package Out-Of-Date" msgstr "Marquer le paquet comme périmé" +#: 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 "" @@ -774,7 +791,7 @@ msgstr "ne peut contenir qu'un seul point, tiret bas ou virgule," #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Merci de confirmer votre mot de passe:" #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -782,7 +799,7 @@ msgstr "L'adresse email n'est pas valide." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Cette adresse email de secours est invalide." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -825,15 +842,15 @@ msgstr "La clé SSH publique, %s%s%s, est déjà utilisée." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "Le CAPTCHA est manquant." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "Le CAPTCHA a expiré. Merci de réessayer." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "LE CAPTCHA que vous avez entré est invalide." #: lib/acctfuncs.inc.php #, php-format @@ -873,6 +890,10 @@ msgstr "Le formulaire de connexion est actuellement désactivé pour votre adres msgid "Account suspended" msgstr "Compte suspendu." +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -958,6 +979,30 @@ msgstr "Erreur en recherchant les détails du paquet." msgid "Package details could not be found." msgstr "Les détails du paquet ne peuvent pas être trouvés." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Vous devez être authentifié avant de pouvoir étiqueter des paquets." @@ -994,6 +1039,10 @@ msgstr "Vous n’avez pas la permission de supprimer des paquets." msgid "You did not select any packages to delete." msgstr "Vous n'avez sélectionné aucun paquet à supprimer." +#: 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 "Les paquets sélectionnés ont été supprimés." @@ -1002,10 +1051,18 @@ msgstr "Les paquets sélectionnés ont été supprimés." msgid "You must be logged in before you can adopt packages." 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 "" + #: 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." +#: 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 "Vous n'avez pas sélectionné de paquet à adopter." @@ -1246,7 +1303,7 @@ msgstr "Éditer le compte de cet utilisateur." #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Lister les commentaires de cet utilisateur" #: template/account_edit_form.php #, php-format @@ -1261,7 +1318,7 @@ msgstr "Cliquez %sici%s pour obtenir les détails de l'utilisateur." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Cliquez %sici%s pour lister les commentaires écrits par ce compte." #: template/account_edit_form.php msgid "required" @@ -1304,30 +1361,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 vous ne cachez pas votre adresse email, elle sera visible par l'ensemble des utilisateurs enregistrés de AUR. Si vous cachez votre adresse email, elle sera uniquement visible des membres de l'équipe AUR." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Adresse email de secours" #: 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 "En option, fournis une seconde adresse email qui servira à la récupération de votre compte au cas où vous perdez l'accès à votre adresse email principale." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Les liens pour changer votre mot de passe sont toujours envoyés à votre adresse email principale ainsi qu'à celle de secours." #: 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 "Votre adresse email de secours n'est visible que par les membres du staff d'Arch Linux, sans prendre en compte le paramètre %s." #: template/account_edit_form.php msgid "Language" @@ -1341,7 +1398,7 @@ msgstr "Fuseau horaire" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Si vous voulez changer de mot de passe, entrez votre nouveau mot de passe et confirmez le en l'écrivant à nouveau" #: template/account_edit_form.php msgid "Re-type password" @@ -1375,21 +1432,21 @@ msgstr "Notifier des changements de propriétaire" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Pour confirmer les changements sur le profil, veuillez s'il vous plaît entrer votre mot de passe :" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Votre mot de passe actuel" #: 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 "Pour protéger AUR contre la création automatique de comptes, nous vous demandons s'il vous plaît de bien vouloir donner le résultat de la commande suivante:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Réponse" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1552,7 +1609,7 @@ msgstr "lecture seule" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "Cliquez pour copier" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1604,12 +1661,12 @@ msgstr "Ajouter un commentaire" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Les identifiants des commits Git faisant référence à des commits dans l'AUR et les URLs sont automatiquement convertis en liens." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "%sLa syntaxe Markdown %sn'est pas entièrement supportée." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1621,7 +1678,7 @@ msgstr "Derniers commentaires" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Commentaires pour" #: template/pkg_comments.php #, php-format @@ -1636,7 +1693,7 @@ msgstr "Commentaire anonyme le %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Commenté sur %sle %s." #: template/pkg_comments.php #, php-format @@ -2222,7 +2279,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] a fusionné {old} [2] dans {new} [3].\n\nSi vous ne souhaitez plus recevoir de notifications à propos du nouveau paquet, rendez-vous sur [3] puis cliquez \"{label}\". " #: scripts/notify.py #, python-brace-format @@ -2243,3 +2300,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "N'oubliez pas de voter sur la proposition {id} [1]. La période de vote se termine dans moins de 48 heures." + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/he.po b/po/he.po index 21ca1c8b..cd4a0f87 100644 --- a/po/he.po +++ b/po/he.po @@ -5,14 +5,14 @@ # Translators: # GenghisKhan , 2016 # Lukas Fleischer , 2011 -# Yaron Shahrabani , 2016-2020 +# Yaron Shahrabani , 2016-2022 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-02-01 14:37+0000\n" -"Last-Translator: Yaron Shahrabani \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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -473,6 +473,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 "לא ניתן למצוא חבילה למיזוג הצבעות ותגובות אליה." @@ -571,6 +577,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 "כנראה שזאת חבילה ממערכת ניהול גרסאות קוד. נא %sלא%s לסמן אותה כפגת תוקף אם גרסת החבילה ב־AUR אינה תואמת להגשה העדכנית ביותר. יש לסמן את החבילה רק אם המקורות הועברו או שיש צורך בשינוי ב־PKGBUILD עקב שינויים עדכניים במאגר המקורי." + #: html/pkgflag.php #, php-format msgid "" @@ -867,6 +881,10 @@ msgstr "כתובת הכניסה מנוטרלת לכתובת ה־ IP שלך, כנ 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 "" @@ -952,6 +970,30 @@ msgstr "שגיאה בקבלת נתוני חבילה." msgid "Package details could not be found." msgstr "נתוני חבילה לא נמצאו." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "עליך להיכנס לפני שיהיה באפשרותך לסמן חבילות." @@ -988,6 +1030,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 +1042,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 "לא בחרת שום חבילות לאימוץ." @@ -1700,7 +1754,7 @@ msgstr "קבוצות" #: template/pkg_details.php msgid "Conflicts" -msgstr "התנגשויות" +msgstr "סתירות" #: template/pkg_details.php msgid "Provides" @@ -2247,3 +2301,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "נא לזכור להצביע בהצעה {id} [1]. ההצבעה תסתיים בעוד פחות מ־48 שעות." + +#: 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 "אין לך הרשאות לשנות את סוג חשבון המשתמש הזה לכדי %s." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "אין בקשות יתומות קיימות שתוקפן פג לאשר עבור %s." + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "שגיאת שרת פנימית" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "הפרטים תועדו ויועברו לבדיקת ההנהלה במהירות האפשרות. אנו מתנצלים על אי הנוחות שנגרמה." + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "שגיאת שרת ה־AUR" + +#: 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 "" diff --git a/po/hi_IN.po b/po/hi_IN.po new file mode 100644 index 00000000..37fd082e --- /dev/null +++ b/po/hi_IN.po @@ -0,0 +1,2336 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +# Panwar108 , 2018,2020-2021 +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: hi_IN\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "पृष्ठ नहीं मिला" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "क्षमा करें, अनुरोधित पृष्ठ अनुपलब्ध है।" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "ध्यान दें" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "Git क्लोन यूआरएल ब्राउज़र में उपयोग नहीं होते हैं।" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "%s का पैकेज-संग्रह प्रतिरूपित करने हेतु, %s को निष्पादित करें।" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "%sयहाँ%s क्लिक कर %s विवरण पृष्ठ पर वापस जाएँ।" + +#: html/503.php +msgid "Service Unavailable" +msgstr "सेवा अनुपलब्ध" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "धैर्य रखें। अनुरक्षण के कारण साइट अभी बंद है। साइट शीघ्र ही चालू हो जाएगी।" + +#: html/account.php +msgid "Account" +msgstr "अकाउंट" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "अकाउंट" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "इस क्षेत्र हेतु आपका अभिगम निषेध है।" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "निर्दिष्ट उपयोक्ता हेतु जानकारी प्राप्त प्राप्त करना विफल।" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "आपके पास अकाउंट संपादन हेतु अनुमति नहीं है।" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "अमान्य कूटशब्द।" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "वर्तमान उपयोक्ता खोजने हेतु यह प्रपत्र उपयोग करें।" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "उपयोक्ता जानकारी देखने हेतु लॉगिन आवश्यक है।" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "प्रस्ताव जोड़ें" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "उपयोक्ता कार्य हेतु अमान्य टोकन।" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "अनुपस्थित उपयोक्ता नाम।" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "%s हेतु प्रस्ताव पहले से दर्ज है।" + +#: html/addvote.php +msgid "Invalid type." +msgstr "अमान्य प्रकार।" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "प्रस्ताव रिक्त नहीं होना चाहिए।" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "नवीन प्रस्ताव निवेदित किया गया।" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "मत हेतु प्रस्ताव निवेदित करें।" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "निवेदक/विश्वसनीय उपयोक्ता" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "(अनावश्यक होने पर रिक्त)" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "प्रकार" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "विश्वसनीय उपयोक्ता जोड़ना" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "विश्वसनीय उपयोक्ता हटाना" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "विश्वसनीय उपयोक्ता हटाना (अघोषित निष्क्रियता)" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "उपनियम संशोधन" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "प्रस्ताव" + +#: html/addvote.php +msgid "Submit" +msgstr "निवेदित करें" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "सह-अनुरक्षक प्रबंधन" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "टिप्पणी संपादन" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "डैशबोर्ड" + +#: html/home.php template/header.php +msgid "Home" +msgstr "होम" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "चिन्हित किए गए मेरे पैकेज" + +#: html/home.php +msgid "My Requests" +msgstr "मेरे अनुरोध" + +#: html/home.php +msgid "My Packages" +msgstr "मेरे पैकेज" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "मेरे द्वारा अनुरक्षित पैकेज" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "सह-अनुरक्षित पैकेज" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 दिशा-निर्देश पढ़ें।" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "योगदान की गई PKGBUILD का %sआर्च पैकेज मानकों%s के अनुरूप होना %sआवश्यक%s है अन्यथा वे हटा दी जाएँगी!" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "अपने पसंदीदा पैकेज हेतु मतदान अवश्य करें!" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "[community] के कुछ पैकेज बाइनरी फाइल के रूप में उपलब्ध हो सकते हैं।" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "अस्वीकरण" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "AUR पैकेज उपयोक्ता निर्मित हैं। अतः उपलब्ध फाइलों का उपयोग सावधानीपूर्वक करें।" + +#: html/home.php +msgid "Learn more..." +msgstr "और जानें..." + +#: html/home.php +msgid "Support" +msgstr "सहायता" + +#: html/home.php +msgid "Package Requests" +msgstr "पैकेज अनुरोध" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "पैकेज विवरण पृष्ठ में %sपैकेज कार्य%s हेतु तीन प्रकार के निवेदन संभव हैं :" + +#: html/home.php +msgid "Orphan Request" +msgstr "निरर्थक पैकेज हेतु अनुरोध" + +#: 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 "पैकेज स्वामित्व निरस्त करने हेतु अनुरोध; अत्यधिक समय तक अनुरक्षक के निष्क्रिय व पैकेज पुराना होने की स्थिति में।" + +#: html/home.php +msgid "Deletion Request" +msgstr "पैकेज हटाने हेतु अनुरोध" + +#: 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." +msgstr "आर्च उपयोक्ता पैकेज-संग्रह से पैकेज हटाने हेतु अनुरोध। कृपया पैकेज उपयोग संबंधी समस्या होने पर इसका उपयोग न करें। उचित होगा कि पैकेज अनुरक्षक से संपर्क करें व आवश्यकता हो तो निरर्थक पैकेज हेतु अनुरोध करें।" + +#: html/home.php +msgid "Merge Request" +msgstr "विलय अनुरोध" + +#: html/home.php +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 "एकाधिक पैकेज का आपस में विलय हेतु अनुरोध। पैकेज नाम बदलाव या विभाजित पैकेज से बदलने की स्थिति में।" + +#: 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 "आप अनुरोध संबंधी चर्चा हेतु %saur-requests%s ईमेल-सूची उपयोग कर सकते हैं। ईमेल-सूची केवल चर्चा हेतु है न कि अनुरोध।" + +#: html/home.php +msgid "Submitting Packages" +msgstr "पैकेज निवेदन" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "AUR में पैकेज निवेदित करने हेतु एसएसएच द्वारा Git उपयोग होती है। अधिक जानकारी हेतु आर्च उपयोक्ता पैकेज-संग्रह आर्च विकी पृष्ठ का %sपैकेज निवेदन%s अनुखंड देखें।" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "AUR हेतु निम्नलिखित एसएसएच एन्क्रिप्शन पहचान चिन्ह उपयोग होते हैं :" + +#: html/home.php +msgid "Discussion" +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 ईमेल-सूची उपयोग करें।" + +#: html/home.php +msgid "Bug Reporting" +msgstr "समस्या हेतु रिपोर्ट" + +#: html/home.php +#, php-format +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 वेब अंतरफलक संबंधी समस्याओं के लिए ही करें। पैकेज समस्याओं हेतु पैकेज अनुरक्षक से संपर्क करें या उपयुक्त पैकेज के पृष्ठ पर टिप्पणी करें।" + +#: html/home.php +msgid "Package Search" +msgstr "पैकेज खोज" + +#: html/index.php +msgid "Adopt" +msgstr "स्वामित्व लें" + +#: html/index.php +msgid "Vote" +msgstr "मतदान करें" + +#: html/index.php +msgid "UnVote" +msgstr "मत वापस लें" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "सूचित करें" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "सूचित न करें" + +#: html/index.php +msgid "UnFlag" +msgstr "अचिन्हित करें" + +#: html/login.php template/header.php +msgid "Login" +msgstr "लॉगिन" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "के तौर पर लॉग-इन हैं : %s" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "लॉगआउट" + +#: html/login.php +msgid "Enter login credentials" +msgstr "लॉगिन जानकारी दर्ज करें" + +#: html/login.php +msgid "User name or primary email address" +msgstr "उपयोक्ता नाम या प्राथमिक ईमेल पता" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "कूटशब्द" + +#: html/login.php +msgid "Remember me" +msgstr "लॉगिन स्मरण करें" + +#: html/login.php +msgid "Forgot Password" +msgstr "कूटशब्द भूल गए" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "एचटीटीपी लॉगिन निष्क्रिय है। लॉगिन करने हेतु कृपया %sएचटीटीपीएस उपयोग%s करें।" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "खोज मानदंड" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "पैकेज" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "पैकेज विवरण प्राप्त करने हेतु त्रुटि।" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "आवश्यक जानकारी अनुपलब्ध।" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "कूटशब्द मेल नहीं खाते।" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "कूटशब्द में %s अक्षर आवश्यक हैं।" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "अमान्य ईमेल।" + +#: html/passreset.php +msgid "Password Reset" +msgstr "कूटशब्द पुनः सेट करें" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "पुष्टिकरण लिंक हेतु अपना ईमेल देखें।" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "कूटशब्द पुनः सेट करना सफल।" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "उपयोक्ता नाम या प्राथमिक ईमेल की पुष्टि करें :" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "नया कूटशब्द दर्ज करें :" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "नए कूटशब्द की पुष्टि करें :" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "जारी रखें" + +#: html/passreset.php +#, php-format +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 "यदि आप पंजीकरण में उपयोग किया गया अपना उपयोक्ता नाम व प्राथमिक ईमेल भूल गए हैं तो %saur-general%s ईमेल-सूची पर संदेश भेजें।" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "उपयोक्ता नाम या प्राथमिक ईमेल दर्ज करें :" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "पैकेज बेस" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "मत व टिप्पणियाँ विलय करने हेतु पैकेज नहीं मिला।" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "पैकेज बेस का स्वयं के साथ विलय संभव नहीं है।" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "चयनित पैकेज हटाएँ नहीं गए, चिन्हित कर पुष्टि करें।" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "पैकेज हटाना" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "पैकेज हटाएँ" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "यह प्रपत्र उपयोग कर पैकेज बेस %s%s%s व निम्नलिखित पैकेज AUR से हटाएँ :" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "पैकेज स्थायी रूप से हट जाएगा।" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "चिन्हित कर कार्य की पुष्टि करें।" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "पैकेज हटाने की पुष्टि करें" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "हटाएँ" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "केवल विश्वसनीय उपयोक्ता व व सॉफ्टवेयर विकासकर्ता ही पैकेज हटा सकते हैं।" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "पैकेज स्वामित्व निरस्त करें" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "यह प्रपत्र उपयोग कर पैकेज बेस %s%s%s में सम्मिलित निम्नलिखित पैकेज हेतु स्वामित्व निरस्त करें :" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "इसे चिन्हित कर पुष्टि करें कि अब आप पैकेज सह-अनुरक्षक बने रहने के इच्छुक नहीं हैं।" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "इसे चिन्हित कर पुष्टि करें कि अब आप पैकेज हेतु अपना स्वामित्व निरस्त कर %s%s%s को स्वामित्व दे रहें हैं।" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "इसे चिन्हित कर पुष्टि करें कि अब आप पैकेज हेतु अपना स्वामित्व निरस्त कर रहें हैं।" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "पैकेज स्वामित्व निरस्त करने हेतु पुष्टि करें" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "स्वामित्व निरस्त करें" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "केवल विश्वसनीय उपयोक्ता व व सॉफ्टवेयर विकासकर्ता ही पैकेज स्वामित्व निरस्त कर सकते हैं।" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "टिप्पणी चिन्हित करें" + +#: html/pkgflag.php +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 "प्रतीत होता है कि यह एक वीसीएस पैकेज है। AUR पैकेज संस्करण का हालिया संचित कार्य से मिलान न होने की स्थिति में कृपया इसे पुराने के रूप में चिन्हित %sन%s करें। स्रोत अंतरण या स्रोत में हालिया परिवर्तनों के कारण PKGBUILD में परिवर्तन होने पर ही इस पैकेज को चिन्हित करें।" + +#: html/pkgflag.php +#, php-format +msgid "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "यह प्रपत्र उपयोग कर पैकेज बेस %s%s%s व निम्नलिखित पैकेज को पुराने के रूप में चिन्हित करें :" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "कृपया यह प्रपत्र समस्या रिपोर्ट हेतु उपयोग %sन%s करें। समस्या हेतु पैकेज पृष्ठ पर टिप्पणी करें।" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "नवीन संस्करण की घोषणा या उपलब्ध फाइल की साइट प्रदान कर टिप्पणी करें कि पैकेज पुराने से रूप में चिन्हित क्यों है।" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "टिप्पणियाँ" + +#: html/pkgflag.php +msgid "Flag" +msgstr "चिन्हित" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "केवल पंजीकृत उपयोक्ता ही पैकेज को पुराने के रूप में चिन्हित कर सकते हैं।" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "पैकेज विलय" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "पैकेज विलय करें" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "यह प्रपत्र उपयोग कर पैकेज बेस %s%s%s का अन्य पैकेज में विलय करें।" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "निम्नलिखित पैकेज हटेंगे :" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "पैकेज विलय उपरांत इसे पूर्ववत करना संभव नहीं है।" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "पैकेज विलय हेतु पैकेज नाम दर्ज करें।" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "इसमें विलय करें :" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "पैकेज विलय पुष्टिकरण" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "विलय करें" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "केवल विश्वसनीय उपयोक्ता व व सॉफ्टवेयर विकासकर्ता ही पैकेज विलय कर सकते हैं।" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "अनुरोध निवेदित करें" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "अनुरोध निरस्त करें" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "पहला" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "पिछला" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "अगला" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "अंतिम" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "अनुरोध" + +#: html/register.php template/header.php +msgid "Register" +msgstr "पंजीकरण" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "यह प्रपत्र उपयोग कर अकाउंट बनाएँ।" + +#: html/tos.php +msgid "Terms of Service" +msgstr "सेवा की शर्तें" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "निम्नलिखित प्रलेखों में परिवर्तन हुए हैं। कृपया ध्यानपूर्वक पढ़ें :" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "संशोधन %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 "विश्वसनीय उपयोक्ता" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "प्रस्ताव विवरण प्राप्त करने में विफल।" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "इस प्रस्ताव हेतु वोट प्रक्रिया बंद है।" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "केवल विश्वसनीय उपयोक्ता ही मतदान कर सकते हैं।" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "स्वयं से संबंधित प्रस्ताव में मतदान निषेध है।" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "इस प्रस्ताव हेतु आपका मत दर्ज है।" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "अमान्य मतदान आईडी।" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "वर्तमान मत" + +#: html/tu.php +msgid "Past Votes" +msgstr "पूर्व मत" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "मतदाता" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "संभवतः अवांछनीय इंटरनेट गतिविधिय के कारण आपके आईपी पते हेतु अकाउंट पंजीकरण निष्क्रिय है। असुविधा के लिए खेद है।" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "अनुपस्थित उपयोक्ता आईडी" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "अमान्य उपयोक्ता नाम।" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "%s से लेकर %s अक्षर होना आवश्यक है" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "आरंभ व अंत अक्षर या अंक से हो" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "केवल एक बिंदु, अंडरस्कोर या हायफ़न ही उपयोग करें।" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "कृपया नए कूटशब्द की पुष्टि करें।" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "अमान्य ईमेल पता।" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "अमान्य बैकअप ईमेल पता।" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "अमान्य होम पृष्ठ, कृपया पूर्ण एचटीटीपी(एस) यूआरएल दर्ज करें।" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "अमान्य पीजीपी कुंजी पहचान चिन्ह।" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "अमान्य सार्वजनिक एसएसएच कुंजी।" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "अकाउंट अनुमतियाँ बढ़ाना संभव नहीं है।" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "असमर्थित भाषा।" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "असमर्थित समयक्षेत्र।" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "उपयोक्ता नाम, %s%s%s, पहले से कार्यरत है।" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "पता, %s%s%s, पहले से कार्यरत है।" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "सार्वजनिक एसएसएच कुंजी, %s%s%s, पहले से कार्यरत है।" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "अनुपस्थित CAPTCHA।" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "CAPTCHA मान्यता समाप्त। पुनः प्रयास करें।" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "अमान्य CAPTCHA उत्तर दर्ज किया गया।" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %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, सफलतापूर्वक बनाया गया।" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "कूटशब्द पुनः सेट करने हेतु कुंजी आपको ईमेल कर दी गई है।" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "अकाउंट उपयोग करने हेतु लॉगिन पर क्लिक करें।" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "अकाउंट, %s%s%s में कोई परिवर्तन नहीं हुआ।" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "अकाउंट,%s%s%s, हेतु परिवर्तन सफल।" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "संभवतः अवांछनीय इंटरनेट गतिविधि के कारण आपके आईपी पते हेतु लॉगिन प्रपत्र अभी निष्क्रिय है। असुविधा के लिए खेद है।" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "कूटशब्द पुनः सेट किया गया। यदि अकाउंट नवीन है तो, पुष्टिकरण ईमेल में दिए गए लिंक द्वारा आरंभिक कूटशब्द सेट करें। अन्यथा %sकूटशब्द पुनः सेट%s पृष्ठ द्वारा पुनः सेट करने हेतु कुंजी प्राप्त करें।" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "अनुचित उपयोक्ता नाम या कूटशब्द।" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "उपयोक्ता सत्र बनाने हेतु त्रुटि।" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "अमान्य ईमेल व पुनः सेट कुंजी संयोजन।" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "कुछ नहीं" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "%s हेतु अकाउंट जानकारी देखें" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "अनुपस्थित पैकेज बेस आईडी या पैकेज बेस नाम।" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "यह टिप्पणी संपादित करना संभव नहीं है।" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "अनुपस्थित टिप्पणी।" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "टिप्पणी रिक्त नहीं होनी चाहिए।" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "टिप्पणी जोड़ी गई।" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "पैकेज जानकारी संपादन हेतु लॉगिन आवश्यक है।" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "अनुपस्थित टिप्पणी आईडी।" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "5 से अधिक कमेंट पिन करना संभव नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "आप इस टिप्पणी को पिन नहीं कर सकते हैं।" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "आप इस टिप्पणी को पिन से नहीं हटा सकते हैं।" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "टिप्पणी पिन की गई।" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "टिप्पणी पिन से हटाई गई।" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "पैकेज विवरण प्राप्त करने हेतु त्रुटि।" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "पैकेज विवरण प्राप्त करने में विफल।" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "पैकेज चिन्हित करने हेतु लॉगिन आवश्यक है।" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "चिन्हित करने हेतु कोई पैकेज चयनित नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "चयनित पैकेज अचिन्हित हैं, कृपया टिप्पणी दर्ज करें।" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "चयनित पैकेज पुराने के रूप में चिन्हित हैं।" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "पैकेज अचिन्हित करने हेतु लॉगिन आवश्यक है।" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "अचिन्हित करने हेतु कोई पैकेज चयनित नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "चयनित पैकेज अचिन्हित किए गए।" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "आपके पास पैकेज हटाने हेतु अनुमति नहीं है।" + +#: lib/pkgbasefuncs.inc.php +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 "चयनित पैकेज हटाए गए।" + +#: lib/pkgbasefuncs.inc.php +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 "स्वामित्व प्राप्त करने हेतु कोई पैकेज चयनित नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "स्वामित्व निरस्त करने हेतु कोई पैकेज चयनित नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "चयनित पैकेज का स्वामित्व प्राप्त किया गया।" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "चयनित पैकेज का स्वामित्व निरस्त किया गया।" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "पैकेज के लिए मतदान करने हेतु लॉगिन आवश्यक है।" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "पैकेज के लिए मतदान वापस लेने हेतु लॉगिन आवश्यक है।" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "मतदान करने हेतु कोई पैकेज चयनित नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "चयनित पैकेज हेतु आपके मत हटाए गए।" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "चयनित पैकेज हेतु आपके मत दर्ज किए गए।" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "अधिसूचना सूची में जोड़ना विफल।" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "आपको %s के लिए टिप्पणियों हेतु अधिसूचना सूची में जोड़ दिया गया है।" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "आपको %s के लिए टिप्पणियों हेतु अधिसूचना सूची में से हटा दिया गया है।" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "आपके पास यह टिप्पणी नहीं हटाने हेतु अनुमति नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "टिप्पणी हटाई नहीं गई।" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "आपके पास यह टिप्पणी हटाने हेतु अनुमति नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "टिप्पणी हटा दी गई।" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "टिप्पणी संपादित की गई।" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "आपके पास इस पैकेज बेस के वर्गीकरण शब्द संपादित करने हेतु अनुमति नहीं है।" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "पैकेज बेस के वर्गीकरण शब्द अपडेट किए गए।" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "आपके पास इस पैकेज बेस के सह-अनुरक्षकों के प्रबंधन हेतु अनुमति नहीं है।" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "अमान्य उपयोक्ता नाम : %s" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "पैकेज बेस के सह-अनुरक्षक अपडेट किए गए।" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "इस हेतु पैकेज विवरण देखें" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "%s आवश्यक है" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "पैकेज अनुरोध करने हेतु लॉगिन आवश्यक है।" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "अमान्य नाम : केवल लोअरकेस अक्षर ही स्वीकार्य हैं।" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "टिप्पणी रिक्त नहीं होनी चाहिए।" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "अमान्य अनुरोध प्रकार।" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "अनुरोध जोड़ना सफल।" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "अमान्य कारण।" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "केवल विश्वसनीय उपयोक्ता व व सॉफ्टवेयर विकासकर्ता ही अनुरोध निरस्त कर सकते हैं।" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "अनुरोध निरस्त करना सफल।" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "यह प्रपत्र उपयोग कर AUR अकाउंट स्थायी रूप से हटाएँ।%s" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "%sचेतावनी%s: यह कार्य पूर्ववत करना संभव नहीं है।" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "हटाने की पुष्टि करें" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "उपयोक्ता नाम" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "अकाउंट प्रकार" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "उपयोक्ता" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "सॉफ्टवेयर विकासकर्ता" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "विश्वसनीय उपयोक्ता व सॉफ्टवेयर विकासकर्ता" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "ईमेल पता" + +#: template/account_details.php +msgid "hidden" +msgstr "अदृश्य" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "वास्तविक नाम" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "होम पृष्ठ" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "आईआरसी उपनाम" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "पीजीपी कुंजी पहचान चिन्ह" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "स्थिति" + +#: template/account_details.php +msgid "Inactive since" +msgstr "तब से निष्क्रिय" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "सक्रिय" + +#: template/account_details.php +msgid "Registration date:" +msgstr "पंजीकरण दिनांक :" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "अज्ञात" + +#: template/account_details.php +msgid "Last Login" +msgstr "अंतिम बार लॉगिन" + +#: template/account_details.php +msgid "Never" +msgstr "कभी नहीं" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "इस उपयोक्ता के पैकेज देखें" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "उपयोक्ता अकाउंट संपादित करें" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "इस उपयोक्ता की टिप्पणियाँ देखें" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "%sयहाँ%s क्लिक कर यह अकाउंट स्थायी रूप से हटाएँ।" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "%sयहाँ%s क्लिक कर उपयोक्ता विवरण देखें।" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "%sयहाँ%s क्लिक कर इस अकाउंट द्वारा की गई टिप्पणियाँ देखें।" + +#: template/account_edit_form.php +msgid "required" +msgstr "आवश्यक है" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "लॉगिन हेतु प्रयुक्त नाम ही उपयोक्ता नाम होता है। अकाउंट निष्क्रिय होने पर भी यह सार्वजनिक रूप से दृश्यमान होगा।" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "निलंबित अकाउंट" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "निष्क्रिय" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "सुनिश्चित करें कि दर्ज किया गया ईमेल पता उचित है, अन्यथा अभिगम निरस्त हो जाएगी।" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "ईमेल पता छुपाएँ" + +#: template/account_edit_form.php +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 "ईमेल पता न छुपाने पर यह सभी पंजीकृत उपयोक्ताओं व छुपाने पर यह केवल आर्च लिनक्स कर्मियों को दृश्यमान होगा।" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "एक वैकल्पिक ईमेल पता भी दर्ज करें ताकि प्राथमिक ईमेल हेतु अभिगम न होने पर भी आप अपना अकाउंट पुनः स्थापित कर सकें।" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "बैकअप ईमेल पता आर्च लिनक्स कर्मियों को सदैव दृश्यमान होता है, यह %s सेटिंग पर निर्भर नहीं है।" + +#: template/account_edit_form.php +msgid "Language" +msgstr "भाषा" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "समयक्षेत्र" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "कूटशब्द बदलने हेतु नया कूटशब्द दर्ज करें व पुनः दर्ज कर उसकी पुष्टि करें।" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "कूटशब्द पुनः दर्ज करें" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "निम्नलिखित जानकारी केवल आर्च उपयोक्ता पैकेज-संग्रह में पैकेज निवेदन करने हेतु ही आवश्यक है।" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "सार्वजनिक एसएसएच कुंजी" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "अधिसूचना सेटिंग्स" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "नवीन टिप्पणी हेतु सूचित करें" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "पैकेज अपडेट हेतु सूचित करें" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "स्वामित्व परिवर्तन हेतु सूचित करें" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "अपना वर्तमान कूटशब्द दर्ज कर, प्रोफाइल परिवर्तनों की पुष्टि करें :" + +#: template/account_edit_form.php +msgid "Your current password" +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 "AUR में स्वतः अकाउंट निर्माण रोकने हेतु कृपया नीचे दी गई कमांड का आउटपुट प्रदान करें :" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "उत्तर" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "अपडेट करें" + +#: template/account_edit_form.php +msgid "Create" +msgstr "बनाएँ" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "पुनः सेट करें" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "आपके खोज मानदंड हेतु कोई परिणाम नहीं मिला।" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "अकाउंट संपादित करें" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "निलंबित" + +#: template/account_search_results.php +msgid "Edit" +msgstr "संपादित करें" + +#: template/account_search_results.php +msgid "Less" +msgstr "कम" + +#: template/account_search_results.php +msgid "More" +msgstr "अधिक" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "कोई अन्य परिणाम नहीं है।" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "यह प्रपत्र उपयोग कर %s%s%s हेतु सह-अनुरक्षक जोड़ें (प्रत्येक पंक्ति में एक उपयोक्ता):" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "उपयोक्ता" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "संचित करें" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "पुराने के रूप में चिन्हित करती टिप्पणी : %s" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "%s%s%s द्वारा %s%s%s को निम्नलिखित कारण से %s%s%s को पुराने के रूप में चिन्हित किया गया :" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "%s%s%s पुराने के रूप में चिन्हित नहीं है।" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "विवरण पर वापस जाएँ" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "प्रतिलिप्याधिकार %s 2004-%d aurweb विकासकर्ता टीम।" + +#: template/header.php +msgid " My Account" +msgstr "मेरा अकाउंट" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "पैकेज संबंधी कार्य" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "PKGBUILD देखें" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "बदलाव देखें" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "स्नैपशॉट डाउनलोड" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "विकी में ढूँढें" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "पुराने के रूप में चिन्हित (%s)" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "पैकेज को पुराने के रूप में चिन्हित करें" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "पैकेज अचिन्हित करें" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "मत हटाएँ" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "इस पैकेज को वोट दें" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "अधिसूचनाएँ निष्क्रिय करें" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "अधिसूचनाएँ सक्रिय करें" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "सह-अनुरक्षक प्रबंधन" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "%d शेष अनुरोध" +msgstr[1] "%d शेष अनुरोध" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "पैकेज स्वामित्व लें" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "पैकेज बेस विवरण" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "Git प्रतिरूपण यूआरएल" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "केवल रीड योग्य" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "कॉपी करने हेतु क्लिक करें" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "वर्गीकरण शब्द" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "निवेदक" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "अनुरक्षक" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "अंतिम पैकेज कर्ता" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "मत" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "लोकप्रियता" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "प्रथम बार निवेदित" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "अंतिम बार अपडेट" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "इस हेतु टिप्पणी संपादन : %s" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "टिप्पणी करें" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "AUR पैकेज-संग्रह व यूआरएल संचय कार्यों की और इंगित करते Git संचय पहचान साधन स्वतः ही लिंक में परिवर्तित कर दिए जाते हैं।" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "%sमार्कडाउन वाक्य-विन्यास%s आंशिक रूप से ही समर्थित है।" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "पिन की गई टिप्पणियाँ" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "नवीनतम टिप्पणियाँ" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "इस हेतु टिप्पणियाँ" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "%s ने %s को टिप्पणी की" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "अज्ञात ने %s को टिप्पणी की" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "%s पैकेज हेतु %s को टिप्पणी की" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "%s ने %s को हटाया" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "%s को हटाया गया" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "%s द्वारा %s को संपादित" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "%s को संपादित" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "टिप्पणी न हटाएँ" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "टिप्पणी हटाएँ" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "टिप्पणी पिन करें" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "टिप्पणी पिन से हटाएँ" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "पैकेज विवरण" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "पैकेज बेस" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "विवरण" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "स्रोत यूआरएल" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "इस हेतु वेबसाइट देखें" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "लाइसेंस" + +#: template/pkg_details.php +msgid "Groups" +msgstr "समूह" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "इनके विरुद्ध" + +#: template/pkg_details.php +msgid "Provides" +msgstr "प्रदान करता है" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "इनके स्थान पर" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "आश्रित पैकेज" + +#: template/pkg_details.php +msgid "Required by" +msgstr "इन हेतु आवश्यक" + +#: template/pkg_details.php +msgid "Sources" +msgstr "स्रोत" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "यह प्रपत्र उपयोग कर पैकेज बेस %s%s%s हेतु अनुरोध निरस्त करें।" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "टिप्पणी होना आवश्यक है। हालाँकि, अनुरोध निरस्त करने के उपरांत भी टिप्पणी करना आवश्यक है।" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "कारण" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "स्वीकृत" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "अस्वीकृत" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "यह प्रपत्र उपयोग कर पैकेज बेस %s%s%s व उसमें सम्मिलित निम्नलिखित पैकेज के विरुद्ध हेतु अनुरोध करें :" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "अनुरोध प्रकार" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "हटाना" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "निरर्थक पैकेज" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "विलय अनुरोध से अभिप्राय है विश्वसनीय उपयोक्ता को पैकेज बेस हटाकर मत व टिप्पणियाँ अन्य पैकेज बेस में अंतरण हेतु निवेदन। पैकेज बेस हटाने से संबंधित पैकेज-संग्रह प्रभावित नहीं होते हैं। सुनिश्चित करें कि आप लक्षित पैकेज का Git वृतांत स्वयं अपडेट करें।" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "निरर्थक पैकेज अनुरोध से अभिप्राय है विश्वसनीय उपयोक्ता को पैकेज स्वामित्व निरस्त करने हेतु निवेदन। अनुरक्षक कार्य अनिवार्यता, अनुरक्षक की अघोषित अनुपस्थिति और स्वयं अनुरक्षक से संपर्क हेतु प्रयास होने पर ही कृपया ऐसा करें।" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "आपके खोज मानदंड हेतु कोई अनुरोध नहीं मिला।" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "%d पैकेज अनुरोध मिला।" +msgstr[1] "%d पैकेज अनुरोध मिले।" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "%d का %d पृष्ठ।" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "पैकेज" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "इन द्वारा निवेदित" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "दिनांक" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "~%d दिन शेष" +msgstr[1] "~%d दिन शेष" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "~%d घंटा शेष" +msgstr[1] "~%d घंटे शेष" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "<1 घंटा शेष" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "स्वीकारें" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "लॉक है" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "निरस्त" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "शेष" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "निरस्त है" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "नाम, विवरण" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "केवल नाम" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "सटीक नाम" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "सटीक पैकेज बेस" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "सह-अनुरक्षक" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "अनुरक्षक, सह-अनुरक्षक" + +#: template/pkg_search_form.php +msgid "All" +msgstr "सभी" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "चिन्हित" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "अचिन्हित" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "नाम" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "मतदान किया गया" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "अंतिम बार परिवर्तन" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "आरोह" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "अवरोह" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "खोज मानदंड दर्ज करें" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "खोज हेतु मापदंड" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "पुराने" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "अनुक्रमण" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "क्रम" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "प्रति पृष्ठ" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "खोजें" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "निरर्थक पैकेज" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "पैकेज सूची प्राप्त करने हेतु त्रुटि।" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "आपके खोज मानदंड हेतु कोई पैकेज नहीं मिला।" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "%d पैकेज मिले।" +msgstr[1] "%d पैकेज मिले।" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "संस्करण" + +#: template/pkg_search_results.php +#, php-format +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 "पैकेज लोकप्रियता की गणना उसके निर्माण उपरांत प्रति दिन से %.2f कारक युक्त प्रत्येक मत के साथ सभी मतों के योग के रूप में की जाती है।" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "हाँ" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "निरर्थक पैकेज" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "कार्य" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "पुराने के रूप में अचिन्हित करें" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "पैकेज स्वामित्व लें" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "पैकेज स्वामित्व निरस्त करें" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "पैकेज हटाएँ" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "पुष्टि करें" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "कोई भी प्रकार" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "खोजें" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "आँकड़ें" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "निरर्थक पैकेज" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "पिछले 7 दिनों में जोड़ें गए पैकेज" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "पिछले 7 दिनों में अपडेट हुए पैकेज" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "पिछले वर्ष में अपडेट हुए पैकेज" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "कभी अपडेट नहीं हुए पैकेज" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "पंजीकृत उपयोक्ता" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "विश्वसनीय उपयोक्ता" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "हालिया अपडेट" + +#: template/stats/updates_table.php +msgid "more" +msgstr "अधिक" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "मेरे आँकड़ें" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "प्रस्ताव विवरण" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "मतदान प्रक्रिया चालू है।" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "निवेदित : %s द्वारा %s" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "अंत" + +#: template/tu_details.php +msgid "Result" +msgstr "परिणाम" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "नहीं" + +#: template/tu_details.php +msgid "Abstain" +msgstr "वोट न करें" + +#: template/tu_details.php +msgid "Total" +msgstr "कुल" + +#: template/tu_details.php +msgid "Participation" +msgstr "भाग लिया" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "विश्वसनीय उपयोक्ताओं द्वारा अंतिम मत" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "अंतिम मत" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "कोई परिणाम नहीं मिला।" + +#: template/tu_list.php +msgid "Start" +msgstr "आरंभ" + +#: template/tu_list.php +msgid "Back" +msgstr "वापस" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "AUR कूटशब्द पुनः सेट करें" + +#: scripts/notify.py +#, python-brace-format +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 "आपके ईमेल पते से संबंधित अकाउंट {user} हेतु कूटशब्द पुनः सेट करने हेतु अनुरोध किया गया है। यदि आप कूटशब्द पुनः सेट करना चाहते हैं तो नीचे दिया लिंक [1] उपयोग करें, अन्यथा इस संदेश को अनदेखा करें।" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "आर्च उपयोक्ता पैकेज-संग्रह में स्वागत है" + +#: 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 "आर्च उपयोक्ता पैकेज-संग्रह में स्वागत है! नवीन अकाउंट हेतु आरंभिक कूटशब्द सेट करने हेतु, नीचे दिया लिंक [1] उपयोग करें। लिंक उपयोग में समस्या होने पर इस लिंक को कॉपी कर ब्राउज़र में पेस्ट करें।" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "{pkgbase} हेतु AUR टिप्पणी" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "{user} [1] द्वारा {pkgbase} [2] में निम्नलिखित टिप्पणी में जोड़ी गई :" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "यदि आप इस पैकेज संबंधी अधिसूचना प्राप्त नहीं करना चाहते हैं तो पैकेज पृष्ठ [2] पर जाकर \"{label}\" चुनें।" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "AUR पैकेज अपडेट : {pkgbase}" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "{user} [1] द्वारा {pkgbase} [2] में निम्नलिखित संचित कार्य किया गया :" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "{pkgbase} के पुराना होने की AUR अधिसूचना" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "{user} [2] द्वारा आपका पैकेज {pkgbase} [1] पुराने के रूप में चिन्हित किया गया है :" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "{pkgbase} हेतु स्वामित्व की AUR अधिसूचना" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "{user} [2] द्वारा पैकेज {pkgbase} [1] का स्वामित्व लिया गया।" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "{user} [2] द्वारा पैकेज {pkgbase} [1] का स्वामित्व निरस्त किया गया।" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "{pkgbase} हेतु सह-अनुरक्षक की AUR अधिसूचना" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "आपको {pkgbase} [1] की सह-अनुरक्षक सूची में जोड़ा गया।" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "आपको {pkgbase} [1] की सह-अनुरक्षक सूची से हटाया गया।" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "AUR पैकेज हटाया गया : {pkgbase}" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "{user} [1] द्वारा {old} [2] का {new} [3] में विलय।\n\n-- \nयदि आप इस नवीन पैकेज संबंधी अधिसूचना प्राप्त नहीं करना चाहते हैं तो [3] पर जाकर \"{label}\" पर क्लिक करें।" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "{user} [1] द्वारा {pkgbase} [2] हटाया गया।\n\nआपको इस पैकेज संबंधी कोई अधिसूचना प्राप्त नहीं होंगी।" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "विश्वसनीय उपयोक्ता मतदान सूचक : प्रस्ताव {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 "प्रस्ताव {id} [1] हेतु अवश्य मतदान करें। मतदान प्रक्रिया 48 घंटे में समाप्त होगी।" + +#: 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 "आपके पास इस उपयोक्ता के अकाउंट प्रकार को %s में परिवर्तित करने हेतु अनुमति नहीं है।" + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "%s स्वीकारनें हेतु कोई निरर्थक पैकेज अनुरोध शेष नहीं है।" + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/hr.po b/po/hr.po index 0a522fb8..4932bd7e 100644 --- a/po/hr.po +++ b/po/hr.po @@ -9,8 +9,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -471,6 +471,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 "" @@ -569,6 +575,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 "" @@ -865,6 +879,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 "" @@ -950,6 +968,30 @@ msgstr "Došlo je do greške prilikom preuzimanja detalja o paketu." msgid "Package details could not be found." msgstr "Nije moguće naći detalje o paketu." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Morate se logirati da bi obilježavali pakete." @@ -986,6 +1028,10 @@ msgstr "" msgid "You did not select any packages to delete." msgstr "Niste odabrali pakete koje želite izbrisati." +#: 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 "Odabrani paketi su izbrisani." @@ -994,10 +1040,18 @@ msgstr "Odabrani paketi su izbrisani." msgid "You must be logged in before you can adopt packages." msgstr "Morate se logirati da bi posvojili pakete." +#: 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 "Morate se logirati da bi se mogli odreknuti paketa." +#: 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 "Niste odabrali pakete koje želite posvojiti." @@ -2240,3 +2294,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/hu.po b/po/hu.po index 1d4cf856..51894457 100644 --- a/po/hu.po +++ b/po/hu.po @@ -7,13 +7,14 @@ # Balló György , 2011,2013-2016 # Balló György , 2016 # Lukas Fleischer , 2011 +# PB, 2020 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,17 +36,17 @@ msgstr "Megjegyzés" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "" +msgstr "A Git tároló klónozásához használatos hivatkozásokat nem böngészővel kell megnyitni." #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "" +msgstr "A(z) %s Git tárolójának klónozásához futtasd a következőt: %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "" +msgstr "Kattints %side%s a(z) %s adatlapjának megnyitásához." #: html/503.php msgid "Service Unavailable" @@ -78,7 +79,7 @@ msgstr "Nincs engedélyed ennek a fióknak a szerkesztéséhez." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Érvénytelen jelszó." #: html/account.php msgid "Use this form to search existing accounts." @@ -168,7 +169,7 @@ msgstr "Hozzászólás szerkesztése" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Vezérlőpult" #: html/home.php template/header.php msgid "Home" @@ -176,11 +177,11 @@ msgstr "Honlap" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "Elavultnak jelölt csomagjaim" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Kérelmeim" #: html/home.php msgid "My Packages" @@ -188,15 +189,15 @@ msgstr "Csomagjaim" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Csomagok keresése, amelyeknek karbantartója vagyok" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Csomagok, amelyeknek társkarbantartója vagyok" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Csomagok keresése, amelyeknek társkarbantartója vagyok" #: html/home.php #, php-format @@ -375,7 +376,7 @@ msgstr "Bejelentkezési adatok megadása" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Felhasználónév vagy elsődleges email cím" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -439,7 +440,7 @@ msgstr "Jelszavad sikeresen visszaállításra került." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Erősítsd meg a felhasználóneved vagy elsődleges email címed:" #: html/passreset.php msgid "Enter your new password:" @@ -458,15 +459,15 @@ msgstr "Folytatás" 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 "Ha elfelejtetted a felhasználóneved és az elsődleges email címed, akkor kérlek küldj üzenetet az %saur-general%s levelezőlistára." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Add meg a felhasználóneved vagy elsődleges email címed:" #: html/pkgbase.php msgid "Package Bases" -msgstr "" +msgstr "Alapcsomagok" #: html/pkgbase.php msgid "" @@ -474,6 +475,12 @@ msgid "" "checkbox." msgstr "A kiválasztott csomagok nem kerültek megtagadásra, ellenőrizd a megerősítő jelölőnégyzetet." +#: 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 "Nem található csomag, amelybe a szavazatok és hozzászólások beolvaszthatók lennének." @@ -538,7 +545,7 @@ msgstr "Használd ezt az űrlapot a(z) %s%s%s alapcsomag megtagadásához, amely msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "A jelölőnégyzet megjelölésével megerősíted, hogy nem akarsz társkarbantartó lenni." #: html/pkgdisown.php #, php-format @@ -572,6 +579,14 @@ msgstr "Hozzászólás jelölése" msgid "Flag Package Out-Of-Date" msgstr "Csomag elvaultnak jelölése" +#: 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 "" @@ -684,21 +699,21 @@ msgstr "Használd ezt a űrlapot felhasználói fiók létrehozására." #: html/tos.php msgid "Terms of Service" -msgstr "" +msgstr "A szolgáltatás feltételei" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "" +msgstr "A következő dokumentumok frissítésre kerültek. Kérlek nézd át őket alaposan:" #: html/tos.php #, php-format msgid "revision %d" -msgstr "" +msgstr "revízió %d" #: html/tos.php msgid "I accept the terms and conditions above." -msgstr "" +msgstr "Elfogadom a feljebb megadott feltételeket." #: html/tu.php template/account_details.php template/header.php msgid "Trusted User" @@ -769,7 +784,7 @@ msgstr "Csak egyetlen pontot, aláhúzást vagy kötőjelet tartalmazhat." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Kérlek erősítsd meg az új jelszavad." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -777,11 +792,11 @@ msgstr "Érvénytelen e-mail cím." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "A másodlagos email cím érvénytelen." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." -msgstr "" +msgstr "A főoldal érvénytelen, kérlek add meg a teljes HTTP(s) URL-t." #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." @@ -801,7 +816,7 @@ msgstr "A nyelv jelenleg nem támogatott." #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." -msgstr "" +msgstr "Az időzóna jelenleg nem támogatott." #: lib/acctfuncs.inc.php #, php-format @@ -820,15 +835,15 @@ msgstr "A(z) %s%s%s nyilvános SSH kulcs már használatban van." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "Hiányzó CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "A CAPTCHA lejárt. Kérlek próbáld újra." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "A megadott CAPTCHA érvénytelen." #: lib/acctfuncs.inc.php #, php-format @@ -868,6 +883,10 @@ msgstr "A bejelentkező űrlap jelenleg letiltásra került az IP címedre, val msgid "Account suspended" msgstr "Fiók felfüggesztve" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -953,6 +972,30 @@ msgstr "Hiba történt a csomag részletes információinak letöltése közben. msgid "Package details could not be found." msgstr "A csomag részletes információi nem találhatók." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Csomagok megjelöléséhez be kell jelentkezned." @@ -989,6 +1032,10 @@ msgstr "Csomagok törléséhez nincs jogosultságod." msgid "You did not select any packages to delete." msgstr "Nem választottál ki egyetlen törlendő csomagot sem." +#: 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 "A kiválasztott csomagok törlése megtörtént." @@ -997,10 +1044,18 @@ msgstr "A kiválasztott csomagok törlése megtörtént." msgid "You must be logged in before you can adopt packages." msgstr "Csomagok örökbefogadásához be kell jelentkezned." +#: 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 "Csomagok megtagadásához be kell jelentkezned." +#: 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 "Nem választottál ki egyetlen örökbe fogadandó csomagot sem." @@ -1233,7 +1288,7 @@ msgstr "Soha" #: template/account_details.php msgid "View this user's packages" -msgstr "A felhasználó csomagjainak megtekintése" +msgstr "Ezen felhasználó csomagjainak megtekintése" #: template/account_details.php msgid "Edit this user's account" @@ -1241,7 +1296,7 @@ msgstr "Ezen felhasználó fiókjának szerkesztése" #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Ezen felhasználó hozzászólásainak megtekintése" #: template/account_edit_form.php #, php-format @@ -1256,7 +1311,7 @@ msgstr "Kattints %side%s a felhasználó részleteihez." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Kattints %side%s a felhasználó hozzászólásainak megtekintéséhez." #: template/account_edit_form.php msgid "required" @@ -1266,7 +1321,7 @@ msgstr "kötelező" msgid "" "Your user name is the name you will use to login. It is visible to the " "general public, even if your account is inactive." -msgstr "" +msgstr "A felhasználóneved a bejelentkezéshez használt név lesz. Látható a mindenki számára még akkor is, ha a fiókod inaktív." #: template/account_edit_form.php template/search_accounts_form.php msgid "Normal user" @@ -1299,30 +1354,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 "A nem rejted el az email címed, akkor minden regisztrált AUR felhasználó láthatja. Ha elrejted az email címed, akkor csak az Arch Linux személyzet láthatja." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Másodlagos email cím" #: 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 "Megadhatsz egy másodlagos email címet, amellyel visszaszerezheted a hozzáférést a fiókodhoz, ha elveszted a hozzáférést az elsődleges email címedhez." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "A jelszó visszaállító linkek az első- és másodlagos email címedre is kiküldésre kerülnek." #: 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 "A másodlagos email címed csak az Arch Linux személyzet számára látható, a(z) %s beállítástól függetlenül." #: template/account_edit_form.php msgid "Language" @@ -1330,13 +1385,13 @@ msgstr "Nyelv" #: template/account_edit_form.php msgid "Timezone" -msgstr "" +msgstr "Időzóna" #: template/account_edit_form.php msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Ha meg akarod változtatni a jelszavad, akkor add meg az új jelszót kétszer a megerősítéshez." #: template/account_edit_form.php msgid "Re-type password" @@ -1370,21 +1425,21 @@ msgstr "Értesítés tulajdonváltozásokról" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "A fiók módosításának megerősítéséhez kérlek add meg a jelenlegi jelszavad:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Jelenlegi jelszavad" #: 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 "Az automatizált regisztrációk kiszűrése érdekében, kérlek add meg a következő parancs kimenetét:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Válasz" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1547,7 +1602,7 @@ msgstr "csak olvasható" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "kattints a másoláshoz" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1593,18 +1648,18 @@ msgstr "Hozzászólás szerkesztése ehhez: %s" #: template/pkg_comment_box.php template/pkg_comment_form.php msgid "Add Comment" -msgstr "Hosszászólás" +msgstr "Hozzászólás" #: template/pkg_comment_form.php msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "A csomag tárolójában lévő Git kommitok azonosítói automatikusan hivatkozássá lesznek alakítva." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "A %sMarkdown leíró nyelv%s részlegesen támogatott." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1616,7 +1671,7 @@ msgstr "Legújabb hozzászólások" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Felhasználó hozzászólásai:" #: template/pkg_comments.php #, php-format @@ -1631,7 +1686,7 @@ msgstr "Névtelen hozzászólás ekkor: %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Hozzászólás a(z) %s csomaghoz %s időpontban" #: template/pkg_comments.php #, php-format @@ -1774,7 +1829,7 @@ 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 " "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 csomagokhoz használható." +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ó." #: template/pkgreq_form.php msgid "" @@ -1794,7 +1849,7 @@ msgstr "Megtagadási kérelem beküldésével megkérsz egy megbízható felhasz #: template/pkgreq_results.php msgid "No requests matched your search criteria." -msgstr "" +msgstr "Nincs a keresési feltételeknek megfelelő kérelem." #: template/pkgreq_results.php #, php-format @@ -1852,7 +1907,7 @@ msgstr "Lezárás" #: template/pkgreq_results.php msgid "Pending" -msgstr "" +msgstr "Függőben lévő" #: template/pkgreq_results.php msgid "Closed" @@ -1876,11 +1931,11 @@ msgstr "Pontos alapcsomag" #: template/pkg_search_form.php msgid "Co-maintainer" -msgstr "" +msgstr "Társkarbantartó" #: template/pkg_search_form.php msgid "Maintainer, Co-maintainer" -msgstr "" +msgstr "Karbantartó, társkarbantartó" #: template/pkg_search_form.php msgid "All" @@ -2117,7 +2172,7 @@ msgstr "Vissza" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "" +msgstr "AUR jelszó visszaállítása" #: scripts/notify.py #, python-brace-format @@ -2125,90 +2180,90 @@ 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 "Jelszó visszaállítást kezdeményeztek a te email címeddel rendelkező {user} felhasználó számára. Ha szeretnéd visszaállítani a jelszavad, akkor nyisd meg a lenti linket [1], egyébként hagyd figyelmen kívül ezt az üzenetet, és semmi sem fog történni." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "Üdvözlünk az Arch User Repositoryban" #: 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 "Üdvözlünk az Arch User Repositoryban! A jelszó beállításához, kérlek nyisd meg a lenti linket [1]. Ha nem működik, akkor próbáld meg kimásolni, és beilleszteni a böngésződ címsorába." #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "AUR hozzászólás: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "{user} [1] hozzászólt a(z) {pkgbase} [2] csomaghoz:" #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Ha többé nem szeretnél értesítést kapni ezen csomaggal kapcsolatban, akkor kérlek nyisd meg a csomag adatlapját [2] és kattints a(z) \"{label}\" feliratra." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "AUR csomag frissítve: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} [1] kommitolt a {pkgbase} [2] csomagba." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "AUR elavult csomag értesítés: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "Az általad karbantartott {pkgbase} [1] csomagot elavultnak jelölte meg {user} [2]:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "AUR tulajdonlási értesítés: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "A(z) {pkgbase} [1] csomagot {user} [2] örökbefogadta." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "A(z) {pkgbase} [1] csomagról {user} [2] lemondott." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "AUR társkarbantartói értesítés: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Hozzáadtak a(z) {pkgbase} [1] csomag társkarbantartóinak listájához." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Eltávolítottak a(z) {pkgbase} [1] csomag társkarbantartóinak listájáról." #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "AUR csomag törölve: {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2217,7 +2272,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] egyesítette a(z) {old} [2] csomagot a(z) {new} [3] csomagba.\n\n--\nHa többé nem szeretnél értesítéseket kapni az új csomaggal kapcsolatban, akkor nyisd meg a [3] hivatkozást és kattints a(z) \"{label}\" feliratra." #: scripts/notify.py #, python-brace-format @@ -2225,16 +2280,61 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] törölte a(z) {pkgbase} [2] csomagt.\n\nTöbbé nem fogsz értesítéseket kapni ezen csomaggal kapcsolatban." #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "Szavazás megbízható felhasználóról: javaslat {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 "Kérlek ne felejts el szavazni a(z) {id} azonosítójú javaslatról. A szavazási kevesebb, mint 48 óra múlva véget ér." + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/id.po b/po/id.po new file mode 100644 index 00000000..75a6c98b --- /dev/null +++ b/po/id.po @@ -0,0 +1,2332 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +# se7entime , 2013 +# se7entime , 2016 +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: id\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "Halaman Tidak Ditemukan" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "Maaf, halaman yang anda minta tidak ada." + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "Layanan Tidak Tersedia" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "Jangan panik! Situs ini sedang tidak aktif karena sedang ada pemeliharaan. Kami akan kembali secepatnya." + +#: html/account.php +msgid "Account" +msgstr "Akun" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "Akun" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "Anda tidak diperbolehkan untuk mengakses area ini." + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "Tidak dapat mengambil informasi untuk pengguna spesifik." + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "Anda tidak mempunyai izin untuk menyunting akun ini." + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "Gunakan formulir berikut untuk mencari akun yang ada." + +#: html/account.php +msgid "You must log in to view user information." +msgstr "Anda harus masuk log untuk melihat informasi pengguna." + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "Tambahkan Proposal" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "Token tidak sah untuk aksi pengguna." + +#: html/addvote.php +msgid "Username does not exist." +msgstr "Nama pengguna tidak ada." + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "%s telah mempunyai proposal yang berjalan untuk mereka." + +#: html/addvote.php +msgid "Invalid type." +msgstr "Tipe tidak sah." + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "Proposal tidak bisa kosong." + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "Proposal baru telah dimasukkan." + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "Masukkan sebuah proposal untuk mengaktifkan voting." + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "Pemohon/TU" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "(kosong jika tidak dapat diterapkan)" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "Tipe" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "Tambahan dari TU" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "Penghapusan dari TU" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "Penghapusan dari TU (tanpa aktifitas yang tidak dideklarasikan)" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "Amandemen Peraturan" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "Proposal" + +#: html/addvote.php +msgid "Submit" +msgstr "Masukkan" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "Kelola Co-maintainer" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "Sunting komentar" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "Beranda" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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." + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +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]." +msgstr "Beberapa paket mungkin disediakan sebagai binabinari di [comunity]." + +#: html/home.php +msgid "DISCLAIMER" +msgstr "DISCLAIMER" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "Bantuan" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "Kriteria Pencarian" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "Paket" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "Galat saat mencoba untuk menerima rincian paket." + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "Paket yang dipilih belum dihapus, periksa kotak centang konfirmasi." + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/id_ID.po b/po/id_ID.po new file mode 100644 index 00000000..d01294c8 --- /dev/null +++ b/po/id_ID.po @@ -0,0 +1,2330 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: id_ID\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/is.po b/po/is.po new file mode 100644 index 00000000..a7a88b04 --- /dev/null +++ b/po/is.po @@ -0,0 +1,2335 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: is\n" +"Plural-Forms: nplurals=2; plural=(n % 10 != 1 || n % 100 == 11);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/it.po b/po/it.po index 5dfdd9fc..436b6459 100644 --- a/po/it.po +++ b/po/it.po @@ -3,8 +3,9 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: +# Fanfurlio Farolfi , 2021-2022 # Giovanni Scafora , 2011-2015 -# Lorenzo Porta , 2014 +# Lorenzo Porta , 2014 # Lukas Fleischer , 2011 # mattia_b89 , 2019 msgid "" @@ -12,8 +13,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -35,17 +36,17 @@ msgstr "Nota" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "" +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 "" +msgstr "Per clonare il reposiroty git di %s, esegui %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "" +msgstr "Clicca %squi%s per tornare alla pagina dei dettagli di %s." #: html/503.php msgid "Service Unavailable" @@ -78,7 +79,7 @@ msgstr "Non hai i permessi necessari per modificare questo account." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Password non valida." #: html/account.php msgid "Use this form to search existing accounts." @@ -168,7 +169,7 @@ msgstr "Edita il commento" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Cruscotto" #: html/home.php template/header.php msgid "Home" @@ -176,11 +177,11 @@ msgstr "Home" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "I miei pacchetti marcati" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Le mie richieste" #: html/home.php msgid "My Packages" @@ -192,7 +193,7 @@ msgstr "Cerca i pacchetti da me mantenuti" #: html/home.php msgid "Co-Maintained Packages" -msgstr "Pacchetti Co-mantenuti" +msgstr "Pacchetti co-mantenuti" #: html/home.php msgid "Search for packages I co-maintain" @@ -375,7 +376,7 @@ msgstr "Inserisci le credenziali di accesso" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Nome utente o indirizzo email primario" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -439,7 +440,7 @@ msgstr "La tua password è stata ripristinata con successo." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Conferma il tuo nome utente o il tuo indirizzo email primario:" #: html/passreset.php msgid "Enter your new password:" @@ -458,15 +459,15 @@ 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 "" +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." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Inserisci il tuo nome utente o il tuo indirizzo email primario:" #: html/pkgbase.php msgid "Package Bases" -msgstr "" +msgstr "Pacchetti base" #: html/pkgbase.php msgid "" @@ -474,6 +475,12 @@ msgid "" "checkbox." msgstr "I pacchetti selezionati non sono stati abbandonati, conferma la tua scelta inserendo un segno di spunta nell'apposita casella di controllo." +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "I pacchetti selezionati non sono stati adottati, conferma la tua scelta inserendo un segno di spunta nell'apposita casella di controllo." + #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." msgstr "Impossibile trovare il pacchetto da unire al suo interno i voti ed i commenti." @@ -538,7 +545,7 @@ msgstr "Usa questo modulo per abbandonare il pacchetto base %s%s%s che contiene msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "Selezionando la casella di controllo, confermi che non vuoi più essere un co-mantenitore del pacchetto." #: html/pkgdisown.php #, php-format @@ -572,6 +579,14 @@ msgstr "Segnala Commento" msgid "Flag Package Out-Of-Date" msgstr "Segnala che il pacchetto non è aggiornato" +#: 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 "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." + #: html/pkgflag.php #, php-format msgid "" @@ -769,7 +784,7 @@ msgstr "Può contenere solo un punto, un trattino basso o un trattino." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Per favore conferma la tua nuova password." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -777,7 +792,7 @@ msgstr "L'indirizzo email non risulta valido." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +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." @@ -820,15 +835,15 @@ msgstr "La chiave pubblica SSH %s%s%s, è già in uso." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "Manca la risposta CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "Il CAPTCHA è scaduto, Per favore riprova." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "La risposta CAPTCHA inserita non è valida." #: lib/acctfuncs.inc.php #, php-format @@ -868,6 +883,10 @@ msgstr "Il modulo d'accesso è attualmente disabilitato per il tuo indirizzo IP, msgid "Account suspended" msgstr "Account sospeso" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "Non hai il permesso per sospendere account." + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -953,6 +972,30 @@ msgstr "Si è verificato un errore durante il recupero dei dettagli del pacchett msgid "Package details could not be found." msgstr "Impossibile trovare i dettagli del pacchetto." +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "Intestazione Referer non valida." + +#: aurweb/routers/packages.py +msgid "You did not select any packages to be notified about." +msgstr "Non hai selezionato nessun pacchetto su cui essere notificato." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "Le notifiche sui pacchetti selezionati sono state attivate." + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "Non hai selezionato nessun pacchetto a cui disattivare la notifica." + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "Un pacchetto fra quelli selezionati non ha notifiche attive." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." +msgstr "Le notifiche sui pacchetti selezionati sono state disattivate." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." msgstr "Devi autenticarti prima di poter contrassegnare i pacchetti." @@ -989,6 +1032,10 @@ msgstr "Non hai il permesso per eliminare i pacchetti." msgid "You did not select any packages to delete." msgstr "Non hai selezionato nessun pacchetto da eliminare." +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Uno dei pacchetti che hai selezionato non esiste." + #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." msgstr "I pacchetti selezionati sono stati eliminati." @@ -997,10 +1044,18 @@ msgstr "I pacchetti selezionati sono stati eliminati." msgid "You must be logged in before you can adopt packages." msgstr "Devi autenticarti prima di poter adottare i pacchetti." +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "Non sei autorizzato ad adottare uno dei pacchetti selezionati." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." msgstr "Devi autenticarti prima di poter abbandonare i pacchetti." +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "Non sei autorizzato ad abbandonare uno dei pacchetti selezionati." + #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." msgstr "Non hai selezionato nessun pacchetto da adottare." @@ -1241,7 +1296,7 @@ msgstr "Modifica l'account di quest'utente" #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Elenca i commenti di quest'utente" #: template/account_edit_form.php #, php-format @@ -1256,7 +1311,7 @@ msgstr "Click %squì%s per il dettagli dell'utente." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Clicca %squi%s per elencare i commenti inseriti da questo account." #: template/account_edit_form.php msgid "required" @@ -1299,30 +1354,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 "Se non nascondi il tuo indirizzo email, sarà visibile a tutti gli utenti registrati su AUR. Se nascondi il tuo indirizzo email, sarà visibile solo ai membri dello staff di Arch Linux." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Indirizzo email di scorta" #: 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 "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." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "I link per resettare la password vengono sempre inviati ad entrambi i tuoi indirizzi email, primario e di scorta." #: 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 "Il tuo indirizzo email di scorta è sempre visibile soltanto ai membri dello staff di Arch Linux, indipendentemente dall'impostazione %s." #: template/account_edit_form.php msgid "Language" @@ -1336,7 +1391,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 "" +msgstr "Se vuoi cambiare la tua password, inseriscine una nuova e confermala inserendola di nuovo." #: template/account_edit_form.php msgid "Re-type password" @@ -1366,25 +1421,25 @@ msgstr "Notifica degli aggiornamenti dei pacchetti" #: template/account_edit_form.php msgid "Notify of ownership changes" -msgstr "" +msgstr "Notifica cambiamenti di proprietà" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Per confermare le modifiche al profilo, per favore inserisci la tua password:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "La tua password attuale" #: 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 "Per proteggere AUR contro la creazione automatica di account, ti chiediamo gentilmente di fornire l'output del seguente comando:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Rispondi" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1444,7 +1499,7 @@ msgstr "Salva" #: template/flag_comment.php #, php-format msgid "Flagged Out-of-Date Comment: %s" -msgstr "Commento Segnalato come Non Aggiornato: %s" +msgstr "Commento per la marcatura come Non Aggiornato: %s" #: template/flag_comment.php #, php-format @@ -1547,7 +1602,7 @@ msgstr "sola lettura" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "clicca per copiare" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1599,12 +1654,12 @@ msgstr "Aggiungi un commento" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Gli identificatori dei commit Git nel repository dei pacchetti AUR e le URL vengono convertite automaticamente in link." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "La %ssintassi Markdown%s è parzialmente supportata." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1616,7 +1671,7 @@ msgstr "Ultimi commenti" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Commenti per" #: template/pkg_comments.php #, php-format @@ -1631,7 +1686,7 @@ msgstr "Commento anonimo su %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Ha commentato sul pacchetto %s su %s" #: template/pkg_comments.php #, php-format @@ -1774,7 +1829,7 @@ 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 " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "" +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." #: template/pkgreq_form.php msgid "" @@ -1782,7 +1837,7 @@ msgid "" "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 "" +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." #: template/pkgreq_form.php msgid "" @@ -1790,11 +1845,11 @@ msgid "" "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 "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." #: template/pkgreq_results.php msgid "No requests matched your search criteria." -msgstr "" +msgstr "Nessuna richiesta corrisponde ai tuoi criteri di ricerca." #: template/pkgreq_results.php #, php-format @@ -1824,8 +1879,8 @@ msgstr "Data" #, php-format msgid "~%d day left" msgid_plural "~%d days left" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "~%d giorno rimanente" +msgstr[1] "~%d giorni rimanenti" #: template/pkgreq_results.php #, php-format @@ -2125,7 +2180,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 "È 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." #: scripts/notify.py msgid "Welcome to the Arch User Repository" @@ -2141,74 +2196,74 @@ msgstr "Benvenuto nel Arch User Repository! Per impostare una password iniziale #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "Commento AUR per {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "{user} [1] ha commentato su {pkgbase} [2]:" #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Se non vuoi più ricevere notifiche su questo pacchetto, per favore vai alla pagina del pacchetto [2] e seleziona \"{label}\"." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "Aggiornamento pachetto base: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +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 "" +msgstr "Notifica AUR per pacchetto {pkgbase} non aggiornato" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "Il tuo pacchetto {pkgbase} [1] è stato marcato come non aggiornato dall'utente {user} [2]:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "Notifica AUR di proprietà per pacchetto {pkgbase} " #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "Il pacchetto {pkgbase} [1] è stato adottato da {user} [2]." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "Il pacchetto {pkgbase} [1] è stato abbandonato da {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "Notifica AUR di co-manutenzione per pacchetto {pkgbase} " #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Sei stato aggiunto alla lista dei co-manutentori di {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Sei stato rimosso dalla lista dei co-manutentori di {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "Pacchetto AUR eliminato: {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2217,7 +2272,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] 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}\"." #: scripts/notify.py #, python-brace-format @@ -2225,16 +2280,61 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] ha eliminato {pkgbase} [2].\n\nNon riceverai più notifiche su questo pacchetto." #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "Promemoria per voto TU: 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." + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Tipo di account 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." + +#: 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." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "Non ci sono richieste di abbandono da accettare per %s." + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "Errore interno del server" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +msgstr "Si è verificato un errore irreversibile." + +#: 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 "I dettagli sono stati registrati e verranno visionati da postmaster velocemente. Ci scusiamo per l'inconvenienza che questo possa aver causato." + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "Errore server AUR" + +#: 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 "" diff --git a/po/ja.po b/po/ja.po index d51319d2..55d056bf 100644 --- a/po/ja.po +++ b/po/ja.po @@ -5,15 +5,15 @@ # Translators: # kusakata, 2013 # kusakata, 2013 -# kusakata, 2013-2018,2020 +# kusakata, 2013-2018,2020-2022 # 尾ノ上卓朗 , 2017 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-02-26 12:49+0000\n" -"Last-Translator: kusakata\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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -474,6 +474,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 "投票やコメントのマージをするパッケージが見つかりません。" @@ -572,6 +578,14 @@ msgstr "コメントのフラグを立てる" msgid "Flag Package Out-Of-Date" msgstr "パッケージの Out-Of-Date フラグを立てる" +#: 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 "このパッケージは VCS パッケージです。AUR 上のパッケージのバージョンと最新のコミットが一致してない場合でも out-of-date フラグを%s立てないでください%s。フラグを立てていいのはソースが変わってしまった場合や上流の変更によって PKGBUILD に変更が必要な場合だけです。" + #: html/pkgflag.php #, php-format msgid "" @@ -868,6 +882,10 @@ msgstr "スパムのために、現在あなたのIPアドレスからはログ 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 "" @@ -953,6 +971,30 @@ msgstr "パッケージの詳細の取得エラー。" msgid "Package details could not be found." msgstr "パッケージの詳細が見つかりませんでした。" +#: 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 msgid "You must be logged in before you can flag packages." msgstr "パッケージのフラグを立てるにはログインする必要があります。" @@ -989,6 +1031,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 "選択されたパッケージが削除されました。" @@ -997,10 +1043,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 "承継するパッケージが選択されていません。" @@ -2233,3 +2287,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "{id} [1] の提案について投票してください。投票期限は48時間以内です。" + +#: 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 "あなたはこのユーザーのアカウントタイプを %s に変更する権限を持っていません。" + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "%s の承認できる孤児リクエストは存在しません。" + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "内部サーバーエラー" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "詳細はログに記録され即座に管理者によって確認されます。ご不便をおかけして申し訳ございません。" + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "AUR サーバーエラー" + +#: 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 "" diff --git a/po/ko.po b/po/ko.po new file mode 100644 index 00000000..808ffe27 --- /dev/null +++ b/po/ko.po @@ -0,0 +1,2330 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ko\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/lt.po b/po/lt.po new file mode 100644 index 00000000..d126f193 --- /dev/null +++ b/po/lt.po @@ -0,0 +1,2345 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: lt\n" +"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" +msgstr[3] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/nb.po b/po/nb.po index 200b6060..1cc090f1 100644 --- a/po/nb.po +++ b/po/nb.po @@ -3,8 +3,8 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: -# Alexander F Rødseth , 2015,2017-2019 -# Alexander F Rødseth , 2011,2013-2014 +# Alexander F. Rødseth , 2015,2017-2019 +# Alexander F. Rødseth , 2011,2013-2014 # Harald H. , 2015 # Kim Nordmo , 2016 # Lukas Fleischer , 2011 @@ -14,8 +14,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -476,6 +476,12 @@ msgid "" "checkbox." msgstr "De valgte pakkene har ikke blitt gjort eierløse, kryss av i boksen for å bekrefte." +#: 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 "Kunne ikke finne pakke for å flette stemmer og kommentarer inn i." @@ -574,6 +580,14 @@ msgstr "Flagg kommentar" msgid "Flag Package Out-Of-Date" msgstr "Rapporter utdatert pakke" +#: 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 "" @@ -870,6 +884,10 @@ msgstr "Inlogging er slått av for din IP adresse, sannsynligvis på grunn av ve msgid "Account suspended" msgstr "Kontoen er stengt" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -955,6 +973,30 @@ msgstr "Kunne ikke finne frem pakkedetaljer." msgid "Package details could not be found." msgstr "Kunne ikke finne pakkedetaljer." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Du må være logget inn for å kunne markere pakker." @@ -991,6 +1033,10 @@ msgstr "Du har ikke rettighetene som skal til for å kunne slette pakker." msgid "You did not select any packages to delete." msgstr "Du valgte ingen pakker for sletting." +#: 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 "De valgte pakkene har blitt slettet." @@ -999,10 +1045,18 @@ msgstr "De valgte pakkene har blitt slettet." msgid "You must be logged in before you can adopt packages." msgstr "Du må være logget inn for å kunne adoptere pakker." +#: 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 "Du må være logget inn for å kunne gjøre pakker eierløse." +#: 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 "Du valgte ingen pakker for adopsjon." @@ -2240,3 +2294,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "Vennligst husk å stemme på forslag {id} [1]. Avstemningsperioden vil være over om mindre enn 48 timer." + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/nb_NO.po b/po/nb_NO.po new file mode 100644 index 00000000..74af6936 --- /dev/null +++ b/po/nb_NO.po @@ -0,0 +1,2337 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +# Kim Nordmo , 2017,2019 +# Lukas Fleischer , 2011 +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nb_NO\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "Finner ikke siden" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "Beklager, siden du har bedt om finnes ikke." + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "Tjeneste utilgjengelig" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "Ingen panikk! Siden er nede grunnet vedlikehold. Vi er snart tilbake." + +#: html/account.php +msgid "Account" +msgstr "Konto" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "Kontoer" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "Du har ikke adgang til dette området." + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "Kunne ikke motta informasjon for den valgte brukeren." + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "Du har ikke adgang til å endre denne kontoen." + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "Bruk dette skjemaet for å søke etter eksisterende kontoer." + +#: html/account.php +msgid "You must log in to view user information." +msgstr "Du må logge inn for å se informasjon om bruker." + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "Legg til forslag" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "Brukernavn finnes ikke." + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "%s har allerede et forslag." + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "Forslag kan ikke være tomt." + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "Nytt forslag innsendt." + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "Send inn et forslag å stemme på." + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "(tom hvis ikke anvendelig)" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "Type" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "Forslag" + +#: html/addvote.php +msgid "Submit" +msgstr "Send inn" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "Hjem" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "Mine pakker" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "Stem" + +#: html/index.php +msgid "UnVote" +msgstr "Av-stem" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "Notifisér" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "Avnotifisér" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "Logg inn" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "Logget inn som: %s" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "Logg ut" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "Passord" + +#: html/login.php +msgid "Remember me" +msgstr "Husk meg" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "Søkekriterier" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "Pakker" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "Feil oppstod under mottagelse av pakkedetaljer." + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "Mangler et nødvendig felt." + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "Passord-felter stemmer ikke overens." + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "Passordet ditt må være minst %s tegn." + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "Neste" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "Bruk dette feltet for å opprette en konto." + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "Betrodd bruker" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "Kunne ikke motta detaljer om forslag." + +#: html/tu.php +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." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "Du kan ikke stemme i et forslag om deg." + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "Du har allerede stemt for dette forslaget." + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "Stemme-ID ikke gyldig." + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "Foreløpige stemmer" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "Mangler bruker-ID" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "Brukernavnet er ikke gyldig." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "Det må være mellom %s og %s tegn langt" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "Start og slutt med en bokstav eller et siffer" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "Kan kun innehold ett punktum, understrek, eller bindestrek." + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "E-post-adressen er ugyldig." + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "Språk er ikke støttet på dette tidspunktet." + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "Du må være logget inn før du kan redigere pakkeinformasjon." + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "Mangler kommentar-ID." + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "Feil oppstod under mottagelse av pakkedetaljer." + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "Pakkedetaljene kunne ikke bli funnet." + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "Du må være logget inn før du kan markere pakker." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "Du valgte ingen pakker å flagge." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "De valgte pakkene har blitt flagget som utdatert." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "Du må være logget inn før du kan fjerne \"utdatert\"-markeringen av pakker." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "Du valgte ingen pakker å flagge bort." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "De valgte pakkene har blitt flagget bort." + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to delete." +msgstr "Du valgte ingen pakker å slette." + +#: 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 "De valgte pakkene har blitt slettet." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can adopt packages." +msgstr "Du må være logget inn før du kan adoptere pakker." + +#: 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 "Du må være logget inn før du kan avslutte eierskap av pakker." + +#: 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 "Du valgte ingen pakker å adoptere." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "Du valgte ingen pakker som skulle fjernes eierskap til." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "Den valgte pakken har blitt adoptert." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "De valgte pakkene eies ikke mer." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "Du må være logget inn før du kan stemme på pakker." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "Du må være logget inn før du av av-stemme pakker." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "Du valgte ingen pakker å stemme på." + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "Stemmene dine har blitt fjernet fra de valgte pakkene." + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "Stemmene dine er talte for de valgte pakkene." + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "Kunne ikke legge til påminnelseslisten." + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "Du har blitt lagt til påminnelselisten for kommentarer for %s." + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "Du har blitt fjernet fra kommentarpåminnelselisten for %s." + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "Du har ikke tilgang til å slette denne kommentaren." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "Kommentar har blitt slettet." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "Invalid navn - kun små bokstaver er tillatte." + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "Brukernavn" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "Konto-type" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "Bruker" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "Utvikler" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "E-post-adresse" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "Ekte navn" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "IRC-nick" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "Status" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "Aktiv" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "Aldri" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "Se denne brukereren sine pakker" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "trengs av" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "Språk" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Tast inn passord på nytt" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "Oppdatér" + +#: template/account_edit_form.php +msgid "Create" +msgstr "Opprett" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "Omstart" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "Ingen søk stemte overens med dine søkekriterier." + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "Endre brukerkonto" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "Suspendert" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "Færre" + +#: template/account_search_results.php +msgid "More" +msgstr "Flere" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "Ingen flere resultater å vise." + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "Vedlikeholder" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "Stemmer" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "Slett kommentar" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "Pakkedetaljer" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "Beskrivelse" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "Avhengigheter" + +#: template/pkg_details.php +msgid "Required by" +msgstr "Trengs av" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "Navn" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "Stemmet" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "Søk etter" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "Sortér med" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "Sorteringsrekkefølge" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "Per side" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "Gå" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "Hjemløse" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "Feil oppstod under mottagelse av pakkeliste." + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "Ingen pakker stemmer overens med dine søkekriterier." + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "Ja" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "hjemløs" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "Handlinger" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "Fjern markering av \"utdatert\"" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "Adoptér pakker" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "Avslutt eierskap av pakker" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "Slett pakker" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "Hvilken som helst type" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "Søk" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "Resultater for forslag" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "Stemmingen er fortsatt aktiv." + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "Innsendt: %s av %s" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "Slutt" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "Nei" + +#: template/tu_details.php +msgid "Abstain" +msgstr "Avhold deg fra" + +#: template/tu_details.php +msgid "Total" +msgstr "Totalt" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "Ingen resultater funnet." + +#: template/tu_list.php +msgid "Start" +msgstr "Start" + +#: template/tu_list.php +msgid "Back" +msgstr "Tilbake" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/nl.po b/po/nl.po index e5dc26d0..282b5b40 100644 --- a/po/nl.po +++ b/po/nl.po @@ -3,8 +3,10 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: -# Heimen Stoffels , 2015 +# Heimen Stoffels , 2021-2022 +# Heimen Stoffels , 2015,2021 # jelly , 2011 +# Kevin Morris , 2022 # Sietse , 2013 # Sietse , 2013 # Wijnand Modderman-Lenstra , 2015 @@ -13,8 +15,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -24,38 +26,38 @@ msgstr "" #: html/404.php msgid "Page Not Found" -msgstr "De pagina kon niet worden gevonden" +msgstr "Pagina niet gevonden" #: html/404.php msgid "Sorry, the page you've requested does not exist." -msgstr "Sorry, de pagina die u heeft aangevraagd bestaat niet." +msgstr "De opgevraagde pagina bestaat niet." #: html/404.php template/pkgreq_close_form.php msgid "Note" -msgstr "Notitie" +msgstr "Opmerking" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "" +msgstr "Git clone-url's horen niet te worden geopend in een webbrowser." #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "" +msgstr "Wilt u de Git-repo van %s klonen? Voer dan %s uit." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "" +msgstr "Klik %shier%s om terug te keren naar de %s informatiepagina." #: html/503.php msgid "Service Unavailable" -msgstr "De dienst is niet beschikbaar" +msgstr "Dienst is niet beschikbaar" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "Raak niet in paniek! De website is uit de lucht wegens werkzaamheden. We zullen snel weer terug in de lucht zijn." +msgstr "Raak niet in paniek: de website is vanwege werkzaamheden uit de lucht. We zijn zo spoedig mogelijk weer terug." #: html/account.php msgid "Account" @@ -67,44 +69,44 @@ msgstr "Accounts" #: html/account.php html/addvote.php msgid "You are not allowed to access this area." -msgstr "U heeft geen toegang tot dit gebied." +msgstr "U heeft geen toegang tot deze sectie." #: html/account.php msgid "Could not retrieve information for the specified user." -msgstr "Kon geen informatie ophalen voor de opgegeven gebruiker." +msgstr "Er kan geen informatie worden opgehaald over de opgegeven gebruiker." #: html/account.php msgid "You do not have permission to edit this account." -msgstr "U heeft geen toestemming om dit account te bewerken." +msgstr "U bent niet bevoegd om dit account te bewerken." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Ongeldig wachtwoord." #: html/account.php msgid "Use this form to search existing accounts." -msgstr "Gebruik dit formulier om bestaande accounts te zoeken." +msgstr "Gebruik dit formulier om te zoeken naar bestaande accounts." #: html/account.php msgid "You must log in to view user information." -msgstr "U moet inloggen om de gebruikersinformatie te bekijken." +msgstr "Log in om gebruikersinformatie te bekijken." #: html/addvote.php template/tu_list.php msgid "Add Proposal" -msgstr "Voorstel toevoegen" +msgstr "Voorstel doen" #: html/addvote.php msgid "Invalid token for user action." -msgstr "Ongeldige token voor gebruikersactie." +msgstr "Ongeldige toegangssleutel voor gebruikersactie." #: html/addvote.php msgid "Username does not exist." -msgstr "De gebruikersnaam bestaat niet." +msgstr "Deze gebruikersnaam bestaat niet." #: html/addvote.php #, php-format msgid "%s already has proposal running for them." -msgstr "%s heeft al een voorstel ervoor lopen." +msgstr "%s heeft hier al een voorstel voor ingediend." #: html/addvote.php msgid "Invalid type." @@ -112,7 +114,7 @@ msgstr "Ongeldig type." #: html/addvote.php msgid "Proposal cannot be empty." -msgstr "Het voorstel mag niet leeg worden gelaten." +msgstr "U mag geen blanco voorstel doen." #: html/addvote.php msgid "New proposal submitted." @@ -120,7 +122,7 @@ msgstr "Het nieuwe voorstel is ingediend." #: html/addvote.php msgid "Submit a proposal to vote on." -msgstr "Een voorstel indienen om op te stemmen." +msgstr "Dien een voorstel in om op te stemmen." #: html/addvote.php msgid "Applicant/TU" @@ -165,23 +167,23 @@ msgstr "Mede-onderhouders beheren" #: html/commentedit.php template/pkg_comments.php msgid "Edit comment" -msgstr "" +msgstr "Opmerking bewerken" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Overzicht" #: html/home.php template/header.php msgid "Home" -msgstr "Startgedeelte" +msgstr "Startpagina" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "Mijn gemarkeerde pakketten" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Mijn verzoeken" #: html/home.php msgid "My Packages" @@ -189,29 +191,29 @@ msgstr "Mijn pakketten" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Zoeken naar pakketten die ik beheer" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Mede-onderhouden pakketten" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +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 bij de AUR! Lees de %sAUR-gebruikersrichtlijnen%s en de %sAUR-ontwikkelaarsrichtlijnen%s voor meer informatie." +msgstr "Welkom op AUR! Bekijk voor meer informatie de %sAUR-gebruikersrichtlijnen%s en %sAUR-ontwikkelaarsrichtlijnen%s." #: html/home.php #, php-format msgid "" "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " "otherwise they will be deleted!" -msgstr "Bijgedragen PKGBUILDs %smoeten%s voldoen aan de %sArch-pakketstandaarden%s, anders zullen ze worden verwijderd!" +msgstr "Bijgedragen PKGBUILDs %smoeten%s voldoen aan de %sArch-pakketstandaarden%s, anders worden ze verwijderd!" #: html/home.php msgid "Remember to vote for your favourite packages!" @@ -219,7 +221,7 @@ 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 kunnen worden geleverd als uitvoerbare bestanden in [community]." +msgstr "Sommige pakketten in [community] kunnen worden aangeleverd als uitvoerbare bestanden." #: html/home.php msgid "DISCLAIMER" @@ -229,11 +231,11 @@ msgstr "DISCLAIMER" msgid "" "AUR packages are user produced content. Any use of the provided files is at " "your own risk." -msgstr "" +msgstr "AUR-pakketten zijn volledig samengesteld door gebruikers. Gebruik hiervan is op eigen risico." #: html/home.php msgid "Learn more..." -msgstr "Meer leren..." +msgstr "Meer informatie…" #: html/home.php msgid "Support" @@ -241,56 +243,56 @@ msgstr "Ondersteuning" #: html/home.php msgid "Package Requests" -msgstr "Pakketaanvragen" +msgstr "Pakketverzoeken" #: html/home.php #, php-format msgid "" "There are three types of requests that can be filed in the %sPackage " "Actions%s box on the package details page:" -msgstr "Er zijn drie typen aanvragen die kunnen worden ingedeeld in het %sPakketacties%s-veld op de pakketdetails-pagina:" +msgstr "Er zijn drie soorten verzoeken die kunnen worden ingediend middels het %sPakketacties%s-veld op de pakketinformatiepagina:" #: html/home.php msgid "Orphan Request" -msgstr "Weesaanvraag" +msgstr "Onteigeningsverzoek" #: 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 "Vragen om het pakket te markeren als wees, bijv. wanneer de onderhouder inactief is en het pakket langere tijd als verouderd gemarkeerd is." +msgstr "Verzoek om het pakket te markeren als onteigend, bijv. als de eigenaar inactief is en het pakket langere tijd als verouderd gemarkeerd is." #: html/home.php msgid "Deletion Request" -msgstr "Verwijderaanvraag" +msgstr "Verwijderingsverzoek" #: 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." -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 zo'n geval contact op met de pakketontwikkelaar en open zo nodig een weesverzoek." +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." #: html/home.php msgid "Merge Request" -msgstr "Aanvraag samenvoegen" +msgstr "Samenvoegverzoek" #: html/home.php 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 "Verzoek om een pakket samen te laten voegen met een ander pakket. Dit kan gebruikt worden als een pakket hernoemd moet worden of vervangen wordt door een opgesplitst pakket." +msgstr "Verzoek om een pakket samen te laten voegen met een ander pakket. Dit kan gebruikt worden als een pakketnaam gewijzigd moet worden of vervangen wordt door een opgesplitst pakket." #: 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 "Indien je het verzoek wilt bediscussiëren, dan kun je de %saur-requests%s maillijst gebruiken. Gebruik deze lijst niet om verzoeken in te dienen." +msgstr "Als u het verzoek wilt bediscussiëren, dan kunt u hiervoor de %saur-requests%s-mailinglijst gebruiken. Gebruik deze lijst niet om verzoeken in te dienen." #: html/home.php msgid "Submitting Packages" -msgstr "Het bijdragen van pakketten" +msgstr "Pakketten bijdragen" #: html/home.php #, php-format @@ -298,15 +300,15 @@ msgid "" "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." -msgstr "Git via SSA is nu in gebruik om pakketten bij te dragen aan de AUR. Zie de %sHet bijdragen van pakketten%s-gedeelte van de Arch User Repository ArchWiki-pagina voor meer details." +msgstr "U dient git via ssh te gebruiken om pakketten bij te dragen aan de AUR. Bekijk voor meer informatie de pagina %sPakketten bijdragen%s op de Arch User Repository ArchWiki-pagina." #: html/home.php msgid "The following SSH fingerprints are used for the AUR:" -msgstr "De volgende SSH-vingerafdrukken worden gebruikt voor de AUR:" +msgstr "De volgende ssh-vingerafdrukken worden gebruikt voor de AUR:" #: html/home.php msgid "Discussion" -msgstr "Discussie" +msgstr "Discussiëren" #: html/home.php #, php-format @@ -314,11 +316,11 @@ 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. Voor discussies gerelateerd aan de ontwikkeling van de AUR web interface, gebruik de %saur-dev%s mailinglijst." +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." #: html/home.php msgid "Bug Reporting" -msgstr "Bug-rapportage" +msgstr "Bugmeldingen" #: html/home.php #, php-format @@ -327,15 +329,15 @@ msgid "" "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 "" +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." #: html/home.php msgid "Package Search" -msgstr "Pakketten zoeken" +msgstr "Zoeken naar pakketten" #: html/index.php msgid "Adopt" -msgstr "Adopteren" +msgstr "Eigenaar worden" #: html/index.php msgid "Vote" @@ -343,19 +345,19 @@ msgstr "Stemmen" #: html/index.php msgid "UnVote" -msgstr "Stem verwijderen" +msgstr "Stem intrekken" #: html/index.php template/pkg_search_form.php template/pkg_search_results.php msgid "Notify" -msgstr "Informeren" +msgstr "Melding bij activiteit" #: html/index.php template/pkg_search_results.php msgid "UnNotify" -msgstr "Niet informeren" +msgstr "Geen melding bij activiteit" #: html/index.php msgid "UnFlag" -msgstr "De-markeren" +msgstr "Demarkeren" #: html/login.php template/header.php msgid "Login" @@ -372,11 +374,11 @@ msgstr "Uitloggen" #: html/login.php msgid "Enter login credentials" -msgstr "Vul uw inloggegevens in" +msgstr "Voer uw inloggegevens in" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Gebruikersnaam of e-mailadres" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -394,7 +396,7 @@ msgstr "Wachtwoord vergeten" #, php-format msgid "" "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." -msgstr "HTTP-inloggen is uitgeschakeld. %sVerander naar HTTPs%s om in te loggen." +msgstr "Inloggen via http is uitgeschakeld. %sSchakel over naar https%s om in te loggen." #: html/packages.php template/pkg_search_form.php msgid "Search Criteria" @@ -407,20 +409,20 @@ msgstr "Pakketten" #: html/packages.php msgid "Error trying to retrieve package details." -msgstr "Fout bij het ophalen van pakket details." +msgstr "De pakketinformatie kan niet worden opgehaald." #: html/passreset.php lib/acctfuncs.inc.php msgid "Missing a required field." -msgstr "Er ontbreekt een verplicht veld." +msgstr "Er is een verplicht veld leeggelaten." #: html/passreset.php lib/acctfuncs.inc.php msgid "Password fields do not match." -msgstr "Wachtwoordvelden komen niet overeen." +msgstr "De wachtwoordvelden komen niet overeen." #: html/passreset.php lib/acctfuncs.inc.php #, php-format msgid "Your password must be at least %s characters." -msgstr "Uw wachtwoord moet minimaal %s karakters lang zijn." +msgstr "Uw wachtwoord dient minimaal %s tekens te bevatten." #: html/passreset.php msgid "Invalid e-mail." @@ -432,15 +434,15 @@ msgstr "Wachtwoordherstel" #: html/passreset.php msgid "Check your e-mail for the confirmation link." -msgstr "Controleer uw e-mail voor de bevestigingslink." +msgstr "Er is een e-mail met bevestigingslink aan u verstuurd." #: html/passreset.php msgid "Your password has been reset successfully." -msgstr "Uw wachtwoord is met succes teruggezet." +msgstr "Uw wachtwoord is hersteld." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Bevestig uw gebruikersnaam of e-mailadres:" #: html/passreset.php msgid "Enter your new password:" @@ -459,25 +461,31 @@ msgstr "Doorgaan" 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 "Als u uw gebruikersnaam en e-mailadres vergeten bent, stuur dan een bericht aan de %saur-general%s-mailinglijst." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Voer uw gebruikersnaam of e-mailadres in:" #: html/pkgbase.php msgid "Package Bases" -msgstr "" +msgstr "Basispakketten" #: html/pkgbase.php msgid "" "The selected packages have not been disowned, check the confirmation " "checkbox." -msgstr "De geselecteerde pakketten zijn niet onteigend, vink het bevestigingsvakje aan." +msgstr "De geselecteerde pakketten zijn niet onteigend. Kruis het bevestigingsvakje aan." + +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "De geselecteerde pakketten zijn niet overgedragen. Kruis het bevestigingsvakje aan." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." -msgstr "Kan geen pakket vinden om stemmen en comments mee samen te voegen" +msgstr "Er is geen pakket gevonden om stemmen en opmerkingen mee samen te voegen." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot merge a package base with itself." @@ -487,7 +495,7 @@ msgstr "Het basispakket kan niet met zichzelf worden samengevoegd." msgid "" "The selected packages have not been deleted, check the confirmation " "checkbox." -msgstr "De geselecteerde pakketten zijn niet gewist, vink het bevestigingsvakje aan." +msgstr "De geselecteerde pakketten zijn niet gewist. Kruis het bevestigingsvakje aan." #: html/pkgdel.php msgid "Package Deletion" @@ -495,26 +503,26 @@ msgstr "Pakketverwijdering" #: html/pkgdel.php template/pkgbase_actions.php msgid "Delete Package" -msgstr "Verwijder Pakket" +msgstr "Pakket verwijderen" #: html/pkgdel.php #, php-format msgid "" "Use this form to delete the package base %s%s%s and the following packages " "from the AUR: " -msgstr "Gebruik dit formulier om basispakket %s%s%s te verwijderen van AUR met inbegrip van de volgende pakketten:" +msgstr "Gebruik dit formulier om het basispakket ‘%s%s%s’, te verwijderen van AUR, evenals de volgende pakketten:" #: html/pkgdel.php msgid "Deletion of a package is permanent. " -msgstr "Verwijdering van een pakket is permanent." +msgstr "De verwijdering is permanent." #: html/pkgdel.php html/pkgmerge.php msgid "Select the checkbox to confirm action." -msgstr "Vink het veld aan om de actie te bevestigen." +msgstr "Kruis het veld aan om de actie te bevestigen." #: html/pkgdel.php msgid "Confirm package deletion" -msgstr "Bevestig pakketverwijdering" +msgstr "Verwijdering bevestigen" #: html/pkgdel.php template/account_delete.php msgid "Delete" @@ -522,7 +530,7 @@ msgstr "Verwijderen" #: html/pkgdel.php msgid "Only Trusted Users and Developers can delete packages." -msgstr "Alleen Trusted Users en Ontwikkelaars kunnen pakketten verwijderen." +msgstr "Alleen Trusted Users en Developers kunnen pakketten verwijderen." #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -533,29 +541,29 @@ msgstr "Pakket onteigenen" msgid "" "Use this form to disown the package base %s%s%s which includes the following" " packages: " -msgstr "Gebruik dit formulier om basispakket %s%s%s te onteigenen met inbegrip van de volgende pakketten:" +msgstr "Gebruik dit formulier om het basispakket ‘%s%s%s’, te onteigenen, evenals de volgende pakketten:" #: html/pkgdisown.php msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "Door het bevestigingsvakje aan te kruisen, stemt u in met het feit dat u geen mede-onderhouder meer bent." #: html/pkgdisown.php #, php-format msgid "" "By selecting the checkbox, you confirm that you want to disown the package " "and transfer ownership to %s%s%s." -msgstr "Door het bevestigingsvakje aan te vinken, stem je in met het onteigenen het pakket en het eigendom over te dragen aan %s%s%s." +msgstr "Door het bevestigingsvakje aan te kruisen, bevestigt u dat u het pakket wilt onteigenen en het wilt overdragen aan %s%s%s." #: html/pkgdisown.php msgid "" "By selecting the checkbox, you confirm that you want to disown the package." -msgstr "Door het bevestigingsvakje aan te vinken, stem je in met het onteigenen van het pakket." +msgstr "Door het bevestigingsvakje aan te kruisen, stemt u in met het onteigenen van het pakket." #: html/pkgdisown.php msgid "Confirm to disown the package" -msgstr "Bevestig de onteigening van het pakket" +msgstr "Bevestig de pakketonteigening" #: html/pkgdisown.php msgid "Disown" @@ -563,40 +571,48 @@ msgstr "Onteigenen" #: html/pkgdisown.php msgid "Only Trusted Users and Developers can disown packages." -msgstr "Alleen Vertouwde Gebruikers en ontwikkelaars kunnen pakketten onteigenen." +msgstr "Alleen Trusted Users en Developers kunnen pakketten onteigenen." #: html/pkgflagcomment.php msgid "Flag Comment" -msgstr "" +msgstr "Markeringsopmerking" #: html/pkgflag.php msgid "Flag Package Out-Of-Date" -msgstr "" +msgstr "Pakket markeren als verouderd" + +#: 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 "Dit lijkt een VCS-pakket te zijn. Markeer dit pakket %sniet%s als verouderd als de pakketversie op AUR niet overeenkomt met de recentste commit. Markeer het alleen als de broncodelocatie gewijzigd is of de PKGBUILD moet worden aangepast." #: html/pkgflag.php #, php-format msgid "" "Use this form to flag the package base %s%s%s and the following packages " "out-of-date: " -msgstr "" +msgstr "Gebruik dit formulier om het basispakket ‘%s%s%s’ te markeren als verouderd, evenals de volgende pakketten:" #: html/pkgflag.php #, php-format msgid "" "Please do %snot%s use this form to report bugs. Use the package comments " "instead." -msgstr "" +msgstr "Gebruik dit formulier %sniet%s om bugs te melden. Bugs kunt u melden in de opmerkingen." #: html/pkgflag.php msgid "" "Enter details on why the package is out-of-date below, preferably including " "links to the release announcement or the new release tarball." -msgstr "" +msgstr "Vertel hieronder duidelijk waarom het pakket verouderd is, bij voorkeur met een link naar de aankondiging van de nieuwe versie of een link naar de tarball." #: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php #: template/pkgreq_results.php msgid "Comments" -msgstr "Commentaar" +msgstr "Opmerkingen" #: html/pkgflag.php msgid "Flag" @@ -604,7 +620,7 @@ msgstr "Markeren" #: html/pkgflag.php msgid "Only registered users can flag packages out-of-date." -msgstr "" +msgstr "Alleen geregistreerde gebruikers kunnen pakketten markeren als verouderd." #: html/pkgmerge.php msgid "Package Merging" @@ -612,28 +628,28 @@ msgstr "Pakketsamenvoeging" #: html/pkgmerge.php template/pkgbase_actions.php msgid "Merge Package" -msgstr "Voeg Pakket Samen" +msgstr "Pakket samenvoegen" #: html/pkgmerge.php #, php-format msgid "Use this form to merge the package base %s%s%s into another package. " -msgstr "Gebruik dit formulier om basispakkete %s%s%s samen te voegen met een ander pakket." +msgstr "Gebruik dit formulier om het basispakket ‘%s%s%s’ samen te voegen met een ander pakket." #: html/pkgmerge.php msgid "The following packages will be deleted: " -msgstr "De volgende pakketten zullen worden verwijderd:" +msgstr "De volgende pakketten worden verwijderd:" #: html/pkgmerge.php msgid "Once the package has been merged it cannot be reversed. " -msgstr "Als dit pakket samengevoegd is kan dit niet teruggedraaid worden." +msgstr "Als dit pakket is samengevoegd, dan kan het proces niet worden teruggedraaid." #: html/pkgmerge.php msgid "Enter the package name you wish to merge the package into. " -msgstr "Geef de naam van het pakket waarmee dit pakket samengevoegd moet worden." +msgstr "Geef de naam op van het pakket waarmee dit pakket moet worden samengevoegd." #: html/pkgmerge.php msgid "Merge into:" -msgstr "Samenvoegen naar:" +msgstr "Samenvoegen met:" #: html/pkgmerge.php msgid "Confirm package merge" @@ -645,15 +661,15 @@ msgstr "Samenvoegen" #: html/pkgmerge.php msgid "Only Trusted Users and Developers can merge packages." -msgstr "Alleen Trusted Users en Ontwikkelaars kunnen pakketten samen voegen." +msgstr "Alleen Trusted Users en Developers kunnen pakketten samenvoegen." #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" -msgstr "" +msgstr "Verzoek indienen" #: html/pkgreq.php template/pkgreq_close_form.php msgid "Close Request" -msgstr "Aanvraag sluiten" +msgstr "Verzoek sluiten" #: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" @@ -673,7 +689,7 @@ msgstr "Laatste" #: html/pkgreq.php template/header.php msgid "Requests" -msgstr "Aanvragen" +msgstr "Verzoeken" #: html/register.php template/header.php msgid "Register" @@ -685,21 +701,21 @@ msgstr "Gebruik dit formulier om een ​​account aan te maken." #: html/tos.php msgid "Terms of Service" -msgstr "" +msgstr "Algemene voorwaarden" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "" +msgstr "De volgende documenten zijn bijgewerkt - lees ze zorgvuldig door:" #: html/tos.php #, php-format msgid "revision %d" -msgstr "" +msgstr "revisie %d" #: html/tos.php msgid "I accept the terms and conditions above." -msgstr "" +msgstr "Ik ga akkoord met de algemene voorwaarden." #: html/tu.php template/account_details.php template/header.php msgid "Trusted User" @@ -707,15 +723,15 @@ msgstr "Trusted User" #: html/tu.php msgid "Could not retrieve proposal details." -msgstr "Voorsteldetails kunnen niet opgehaald worden." +msgstr "Het voorstel kan niet worden geladen." #: html/tu.php msgid "Voting is closed for this proposal." -msgstr "Stemmen is gesloten voor dit voorstel." +msgstr "U kunt niet meer stemmen op dit voorstel." #: html/tu.php msgid "Only Trusted Users are allowed to vote." -msgstr "Alleen Trusted Users kunnen stemmen." +msgstr "Alleen Trusted Users zijn bevoegd om te stemmen." #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -745,7 +761,7 @@ msgstr "Stemmers" msgid "" "Account registration has been disabled for your IP address, probably due to " "sustained spam attacks. Sorry for the inconvenience." -msgstr "Registratie is uitgeschakeld voor jouw IP adres, waarschijnlijk vanwege langdurige spam aanvallen. Excuses voor het ongemak." +msgstr "Registratie is uitgeschakeld op uw IP-adres, waarschijnlijk vanwege langdurige spamaanvallen. Excuses voor het ongemak." #: lib/acctfuncs.inc.php msgid "Missing User ID" @@ -758,19 +774,19 @@ msgstr "De gebruikersnaam is ongeldig." #: lib/acctfuncs.inc.php #, php-format msgid "It must be between %s and %s characters long" -msgstr "Het moet tussen de %s en %s karakters lang zijn" +msgstr "Het moet tussen de %s en %s tekens bevatten" #: lib/acctfuncs.inc.php msgid "Start and end with a letter or number" -msgstr "Beginnen en eindigen met een letter of nummer" +msgstr "Begin en eindig met een letter of nummer" #: lib/acctfuncs.inc.php msgid "Can contain only one period, underscore or hyphen." -msgstr "Kan maar één punt, komma of koppelteken bevatten." +msgstr "Mag maar één punt, komma of koppelteken bevatten." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Bevestig uw nieuwe wachtwoord." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -778,15 +794,15 @@ msgstr "Het e-mailadres is ongeldig." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Het tweede e-mailadres is ongeldig." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." -msgstr "" +msgstr "Het website-adres is ongeldig. Geef een volledige http(s)-url op." #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." -msgstr "De vingerafdruk van de PGP sleutel is ongeldig." +msgstr "De PGP-vingerafdruk is ongeldig." #: lib/acctfuncs.inc.php msgid "The SSH public key is invalid." @@ -794,100 +810,104 @@ msgstr "De publieke SSH-sleutel is ongeldig." #: lib/acctfuncs.inc.php msgid "Cannot increase account permissions." -msgstr "Kan de account permissies niet verhogen." +msgstr "De accountbevoegdheden kunnen niet worden uitgebreid." #: lib/acctfuncs.inc.php msgid "Language is not currently supported." -msgstr "Taal wordt momenteel niet ondersteund." +msgstr "Deze taal wordt momenteel niet ondersteund." #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." -msgstr "" +msgstr "Deze tijdzone wordt momenteel niet ondersteund." #: lib/acctfuncs.inc.php #, php-format msgid "The username, %s%s%s, is already in use." -msgstr "De gebruikersnaam %s%s%s is al in gebruik." +msgstr "De gebruikersnaam, %s%s%s, is al in gebruik." #: lib/acctfuncs.inc.php #, php-format msgid "The address, %s%s%s, is already in use." -msgstr "Het adres %s%s%s is al in gebruik." +msgstr "Het adres, %s%s%s, is al in gebruik." #: lib/acctfuncs.inc.php #, php-format msgid "The SSH public key, %s%s%s, is already in use." -msgstr "De SSH publieke sleutel, %s%s%s, is al ingebruik." +msgstr "De publieke ssh-sleutel, %s%s%s, is al in gebruik." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "U heeft de captcha niet ingevuld." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "Deze captcha is verlopen - probeer het opnieuw." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "Het opgegeven captcha-antwoord is ongeldig." #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." -msgstr "Fout bij het aanmaken van account %s%s%s." +msgstr "Er is een fout opgetreden tijdens het aanmaken van het account ‘%s%s%s’." #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully created." -msgstr "Het account %s%s%s is met succes aangemaakt." +msgstr "Het account, %s%s%s, is aangemaakt." #: lib/acctfuncs.inc.php msgid "A password reset key has been sent to your e-mail address." -msgstr "Een sleutel voor het resetten van je wachtwoord is verstuurd naar je e-mail adres." +msgstr "Er is een wachtwoordherstelsleutel verstuurd naar uw e-mailadres." #: lib/acctfuncs.inc.php msgid "Click on the Login link above to use your account." -msgstr "Klik op de Login link hierboven om je account te gebruiken" +msgstr "Klik op bovenstaande inloglink om uw account in gebruik te nemen." #: lib/acctfuncs.inc.php #, php-format msgid "No changes were made to the account, %s%s%s." -msgstr "Geen veranderingen zijn gemaakt aan het account, %s%s%s." +msgstr "Er zijn geen aanpassingen gedaan aan het account ‘%s%s%s’." #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully modified." -msgstr "Het account %s%s%s is met succes aangepast." +msgstr "Het account, %s%s%s, is aangepast." #: lib/acctfuncs.inc.php msgid "" "The login form is currently disabled for your IP address, probably due to " "sustained spam attacks. Sorry for the inconvenience." -msgstr "Het log-in formulier is uitgeschakeld voor je IP adres, waarschijnlijk vanwege langdurige spam-aanvallen. Excuses voor het ongemak." +msgstr "Het inlogformulier is uitgeschakeld op uw IP-adres, waarschijnlijk vanwege langdurige spamaanvallen. Excuses voor het ongemak." #: lib/acctfuncs.inc.php msgid "Account suspended" msgstr "Account is geschorst" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "U bent niet bevoegd om accounts te schorsen." + #: lib/acctfuncs.inc.php #, php-format msgid "" "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 %sPassword Reset%s page." -msgstr "Je wachtwoord is gereset. Als je net een nieuwe account hebt aangemaakt, gebruik dan de link uit de bevestigingsmail om een wachtwoord te genereren. In andere gevallen, vraag een herstelsleutel aan op de %sPassword Reset%s pagina." +msgstr "Uw wachtwoord is hersteld. Als u net een nieuw account heeft aangemaakt, gebruik dan de link uit de bevestigingsmail om een wachtwoord te genereren. Als dat niet zo is, vraag dan een herstelsleutel aan op de %swachtwoordherstel%spagina." #: lib/acctfuncs.inc.php msgid "Bad username or password." -msgstr "Verkeerd gebruiker of wachtwoord" +msgstr "Onjuiste gebruikersnaam of wachtwoord" #: lib/acctfuncs.inc.php msgid "An error occurred trying to generate a user session." -msgstr "Er is een fout opgetreden bij het aanmaken van een gebruikerssessie." +msgstr "Er is een fout opgetreden tijdens het aanmaken van een gebruikerssessie." #: lib/acctfuncs.inc.php msgid "Invalid e-mail and reset key combination." -msgstr "Ongeldige e-mail en reset-toets combinatie." +msgstr "Ongeldige e-mailadres- en herstelsleutel-combinatie." #: lib/aur.inc.php template/pkg_details.php msgid "None" @@ -896,75 +916,99 @@ msgstr "Geen" #: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php #, php-format msgid "View account information for %s" -msgstr "Toon accountinformatie voor %s" +msgstr "Bekijk accountinformatie van %s" #: lib/aurjson.class.php msgid "Package base ID or package base name missing." -msgstr "" +msgstr "De basispakket-id of -naam ontbreekt." #: lib/aurjson.class.php lib/pkgbasefuncs.inc.php msgid "You are not allowed to edit this comment." -msgstr "" +msgstr "U bent niet bevoegd om deze opmerking te bewerken." #: lib/aurjson.class.php msgid "Comment does not exist." -msgstr "" +msgstr "Deze opmerking bestaat niet." #: lib/pkgbasefuncs.inc.php msgid "Comment cannot be empty." -msgstr "" +msgstr "U mag geen blanco opmerking achterlaten." #: lib/pkgbasefuncs.inc.php msgid "Comment has been added." -msgstr "Opmerking is toegevoegd." +msgstr "De opmerking is toegevoegd." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can edit package information." -msgstr "Je moet ingelogd zijn voordat je pakketinformatie kunt bewerken." +msgstr "Log in om pakketinformatie aan te passen." #: lib/pkgbasefuncs.inc.php msgid "Missing comment ID." -msgstr "Missende comment ID." +msgstr "Ontbrekende opmerkings-ID." #: lib/pkgbasefuncs.inc.php msgid "No more than 5 comments can be pinned." -msgstr "" +msgstr "Er kunnen max. 5 opmerkingen worden vastgezet." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to pin this comment." -msgstr "" +msgstr "U bent niet bevoegd om deze opmerking vast te maken." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to unpin this comment." -msgstr "" +msgstr "U bent niet bevoegd om deze opmerking los te maken." #: lib/pkgbasefuncs.inc.php msgid "Comment has been pinned." -msgstr "" +msgstr "De opmerking is vastgemaakt." #: lib/pkgbasefuncs.inc.php msgid "Comment has been unpinned." -msgstr "" +msgstr "De opmerking is losgemaakt." #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Error retrieving package details." -msgstr "Fout bij het ophalen van pakketdetails." +msgstr "De pakketinformatie kan niet worden geladen." #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Package details could not be found." -msgstr "Pakketdetails kunnen niet gevonden worden." +msgstr "Er is geen pakketinformatie gevonden." + +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "Onjuiste doorverwijskop." + +#: aurweb/routers/packages.py +msgid "You did not select any packages to be notified about." +msgstr "U heeft geen pakketten geselecteerd om meldingen over te ontvangen." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "Meldingen omtrent de geselecteerde pakketten zijn ingeschakeld." + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "U heeft geen pakketten geselecteerd om meldingen van uit te schakelen." + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "Bij één van de geselecteerde pakketten zijn meldingen niet ingeschakeld." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." +msgstr "Meldingen omtrent de geselecteerde pakketten zijn uitgeschakeld." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." -msgstr "Je moet ingelogd zijn voordat je pakketten kunt markeren." +msgstr "Log in om pakketten te markeren." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to flag." -msgstr "Je hebt geen pakketten geselecteerd om te markeren." +msgstr "U heeft geen te markeren pakketten geselecteerd." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have not been flagged, please enter a comment." -msgstr "" +msgstr "De geselecteerde pakketten zijn nog niet gemarkeerd. Laat een opmerking achter." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been flagged out-of-date." @@ -972,23 +1016,27 @@ msgstr "De geselecteerde pakketten zijn gemarkeerd als verouderd." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can unflag packages." -msgstr "Je moet ingelogd zijn voordat je een markering voor een pakket kunt verwijderen." +msgstr "Log in om pakketten te demarkeren." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to unflag." -msgstr "Je hebt geen pakketten geselecteerd om de markering van te verwijderen." +msgstr "U heeft geen te demarkeren pakketten geselecteerd." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been unflagged." -msgstr "De markering voor de geselecteerde pakketten is verwijderd." +msgstr "De geselecteerde pakketten zijn gedemarkeerd." #: lib/pkgbasefuncs.inc.php msgid "You do not have permission to delete packages." -msgstr "Je hebt geen toestemming om pakketten te verwijderen." +msgstr "U bent niet bevoegd om pakketten te verwijderen." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to delete." -msgstr "Je hebt geen pakketten geselecteerd om te verwijderen." +msgstr "U hebt geen te verwijderen pakketten geselecteerd." + +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Eén van de geselecteerde pakketten bestaat niet." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." @@ -996,23 +1044,31 @@ msgstr "De geselecteerde pakketten zijn verwijderd." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can adopt packages." -msgstr "Je moet ingelogd zijn voordat je pakketten kunt adopteren." +msgstr "Log in om eigenaar van pakketten te kunnen worden." + +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "U bent niet bevoegd om één van de geselecteerde pakketten over te nemen." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." -msgstr "Je moet ingelogd zijn voordat je pakketten kunt onteigenen." +msgstr "Log in om pakketten te onteigenen." + +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "U bent niet bevoegd om één van de geselecteerde pakketten te onteigenen." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." -msgstr "Je hebt geen pakketten geselecteerd om te adopteren." +msgstr "U heeft geen over te nemen pakketten geselecteerd." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to disown." -msgstr "Je hebt geen pakketten geselecteerd om te onteigenen." +msgstr "U heeft geen te onteigenen pakketten geselecteerd." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been adopted." -msgstr "De geselecteerde pakketten zijn geadopteerd." +msgstr "De geselecteerde pakketten zijn overgedragen." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been disowned." @@ -1020,69 +1076,69 @@ msgstr "De geselecteerde pakketten zijn onteigend." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can vote for packages." -msgstr "Je moet ingelogd zijn voordat je kunt stemmen op pakketten." +msgstr "Log in om te stemmen op pakketten." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can un-vote for packages." -msgstr "Je moet ingelogd zijn voordat je je stem kunt verwijderen." +msgstr "Log in om stemmen in te trekken." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to vote for." -msgstr "Je hebt geen pakketten geselecteerd om op te stemmen." +msgstr "U heeft geen pakketten geselecteerd om op te stemmen." #: lib/pkgbasefuncs.inc.php msgid "Your votes have been removed from the selected packages." -msgstr "Je stem is verwijderd voor de geselecteerde pakketten." +msgstr "Uw stem op de geselecteerde pakketten is ingetrokken." #: lib/pkgbasefuncs.inc.php msgid "Your votes have been cast for the selected packages." -msgstr "Je hebt gestemd voor de geselecteerde pakketten." +msgstr "U heeft gestemd op de geselecteerde pakketten." #: lib/pkgbasefuncs.inc.php msgid "Couldn't add to notification list." -msgstr "Kon niet toevoegen aan notificatielijst." +msgstr "U kunt niet worden toegevoegd aan de meldingslijst." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been added to the comment notification list for %s." -msgstr "Je bent toegevoegd aan de comment notificatielijst voor %s." +msgstr "U bent toegevoegd aan de meldingslijst met opmerkingen bij %s." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been removed from the comment notification list for %s." -msgstr "Je bent verwijderd van de comment notificatielijst voor %s." +msgstr "U bent toegevoegd van de meldingslijst met opmerkingen bij %s." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to undelete this comment." -msgstr "" +msgstr "U bent niet bevoegd om deze opmerking terug te plaatsen." #: lib/pkgbasefuncs.inc.php msgid "Comment has been undeleted." -msgstr "" +msgstr "De opmerking is teruggeplaatst." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to delete this comment." -msgstr "Je hebt geen toestemming deze comment te verwijderen." +msgstr "U bent niet bevoegd om deze opmerking te verwijderen." #: lib/pkgbasefuncs.inc.php msgid "Comment has been deleted." -msgstr "Comment is verwijderd." +msgstr "De opmerking is verwijderd." #: lib/pkgbasefuncs.inc.php msgid "Comment has been edited." -msgstr "" +msgstr "De opmerking is bewerkt." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to edit the keywords of this package base." -msgstr "Je hebt geen toestemming de sleutelwoorden van dit basispakket aan te passen." +msgstr "U bent niet bevoegd om de trefwoorden van dit basispakket aan te passen." #: lib/pkgbasefuncs.inc.php msgid "The package base keywords have been updated." -msgstr "De sleutelwoorden van het basispakket zijn bijgewerkt." +msgstr "De trefwoorden van dit basispakket zijn bijgewerkt." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to manage co-maintainers of this package base." -msgstr "Je hebt geen toestemming om de mede-eigenaren van dit basispakket aan te passen." +msgstr "U bent niet bevoegd om de mede-onderhouders van dit basispakket aan te passen." #: lib/pkgbasefuncs.inc.php #, php-format @@ -1091,20 +1147,20 @@ msgstr "Ongeldige gebruikersnaam: %s" #: lib/pkgbasefuncs.inc.php msgid "The package base co-maintainers have been updated." -msgstr "De mede-onderhouders van het basispakket zijn bijgewerkt." +msgstr "De mede-onderhouders van dit basispakket zijn bijgewerkt." #: lib/pkgfuncs.inc.php template/pkgbase_details.php msgid "View packages details for" -msgstr "Toon pakketdetails voor" +msgstr "Bekijk pakketinformatie van" #: lib/pkgfuncs.inc.php #, php-format msgid "requires %s" -msgstr "" +msgstr "vereist %s" #: lib/pkgreqfuncs.inc.php msgid "You must be logged in to file package requests." -msgstr "Je moet ingelogd zijn voordat je pekketverzoeken kunt indienen." +msgstr "Log in om pakketverzoeken in te dienen." #: lib/pkgreqfuncs.inc.php msgid "Invalid name: only lowercase letters are allowed." @@ -1112,15 +1168,15 @@ msgstr "Ongeldige naam: alleen kleine letters zijn toegestaan." #: lib/pkgreqfuncs.inc.php msgid "The comment field must not be empty." -msgstr "Het commentaarveld mag niet leeg zijn." +msgstr "U mag geen blanco opmerking achterlaten." #: lib/pkgreqfuncs.inc.php msgid "Invalid request type." -msgstr "Ongeldig verzoektype." +msgstr "Ongeldig verzoek." #: lib/pkgreqfuncs.inc.php msgid "Added request successfully." -msgstr "Verzoek succesvol toegevoegd." +msgstr "Het verzoek is toegevoegd." #: lib/pkgreqfuncs.inc.php msgid "Invalid reason." @@ -1128,25 +1184,25 @@ msgstr "Ongeldige reden." #: lib/pkgreqfuncs.inc.php msgid "Only TUs and developers can close requests." -msgstr "Alleen TUs en ontwikkelaars mogen verzoeken sluiten." +msgstr "Alleen TU's en ontwikkelaars mogen verzoeken sluiten." #: lib/pkgreqfuncs.inc.php msgid "Request closed successfully." -msgstr "Verzoek succesvol gesloten." +msgstr "Het verzoek is gesloten." #: template/account_delete.php #, php-format msgid "You can use this form to permanently delete the AUR account %s." -msgstr "Gebruik dit formulier om permanent AUR account %s te verwijderen." +msgstr "Gebruik dit formulier om het AUR-account ‘%s’ permanent te verwijderen." #: template/account_delete.php #, php-format msgid "%sWARNING%s: This action cannot be undone." -msgstr "%sWAARSCHUWING%s: Deze actie kan niet ongedaan worden gemaakt." +msgstr "%sWAARSCHUWING%s: deze actie kan niet ongedaan worden gemaakt!" #: template/account_delete.php msgid "Confirm deletion" -msgstr "Bevestig verwijdering" +msgstr "Verwijdering bevestigen" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php @@ -1156,7 +1212,7 @@ msgstr "Gebruikersnaam" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Account Type" -msgstr "Account Type" +msgstr "Soort account" #: template/account_details.php template/tu_details.php #: template/tu_last_votes_list.php template/tu_list.php @@ -1171,16 +1227,16 @@ msgstr "Ontwikkelaar" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Trusted User & Developer" -msgstr "Trusted User & Ontwikkelaar" +msgstr "Trusted User en ontwikkelaar" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Email Address" -msgstr "E-mail adres" +msgstr "E-mailadres" #: template/account_details.php msgid "hidden" -msgstr "" +msgstr "verborgen" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php @@ -1189,17 +1245,17 @@ msgstr "Echte naam" #: template/account_details.php template/account_edit_form.php msgid "Homepage" -msgstr "" +msgstr "Website" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php msgid "IRC Nick" -msgstr "IRC Nick" +msgstr "IRC-bijnaam" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php msgid "PGP Key Fingerprint" -msgstr "PGP sleutel vingerafdruk" +msgstr "PGP-vingerafdruk" #: template/account_details.php template/account_search_results.php #: template/pkgreq_results.php @@ -1208,7 +1264,7 @@ msgstr "Status" #: template/account_details.php msgid "Inactive since" -msgstr "Geen activiteit sinds" +msgstr "Inactief sinds" #: template/account_details.php template/account_search_results.php msgid "Active" @@ -1216,7 +1272,7 @@ msgstr "Actief" #: template/account_details.php msgid "Registration date:" -msgstr "" +msgstr "Registratiedatum:" #: template/account_details.php template/pkgbase_details.php #: template/pkg_details.php template/pkgreq_results.php @@ -1226,7 +1282,7 @@ msgstr "onbekend" #: template/account_details.php msgid "Last Login" -msgstr "Laatste Login" +msgstr "Laatst ingelogd" #: template/account_details.php msgid "Never" @@ -1234,30 +1290,30 @@ msgstr "Nooit" #: template/account_details.php msgid "View this user's packages" -msgstr "Bekijk gebruiker zijn pakketten" +msgstr "Bekijk pakketten van deze gebruiker" #: template/account_details.php msgid "Edit this user's account" -msgstr "Bewerk account van deze gebruiker" +msgstr "Bewerk het account van deze gebruiker" #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Toon de opmerkingen van deze gebruiker" #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." -msgstr "Klik %shier%s als u dit account permanent wilt verwijderen." +msgstr "Klik %shier%s om dit account permanent te verwijderen." #: template/account_edit_form.php #, php-format msgid "Click %shere%s for user details." -msgstr "" +msgstr "Klik %shier%s om de gebruikersgegevens te bekijken." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Klik %shier%s om de opmerkingen te bekijken die geplaatst zijn door dit account." #: template/account_edit_form.php msgid "required" @@ -1267,7 +1323,7 @@ msgstr "verplicht" msgid "" "Your user name is the name you will use to login. It is visible to the " "general public, even if your account is inactive." -msgstr "" +msgstr "Uw gebruikersnaam gebruikt u om in te loggen. Deze naam is openbaar, zelfs als uw account inactief is." #: template/account_edit_form.php template/search_accounts_form.php msgid "Normal user" @@ -1275,11 +1331,11 @@ msgstr "Normale gebruiker" #: template/account_edit_form.php template/search_accounts_form.php msgid "Trusted user" -msgstr "Trusted user" +msgstr "Trusted User" #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" -msgstr "Account Geschorst" +msgstr "Account geschorst" #: template/account_edit_form.php msgid "Inactive" @@ -1289,41 +1345,41 @@ msgstr "Inactief" msgid "" "Please ensure you correctly entered your email address, otherwise you will " "be locked out." -msgstr "" +msgstr "Zorg er voor dat u het e-mailadres goed hebt ingevoerd, anders wordt u buitengesloten." #: template/account_edit_form.php msgid "Hide Email Address" -msgstr "" +msgstr "E-mailadres verbergen" #: template/account_edit_form.php 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 "Als u uw e-mailadres níet verbergt, dan is het zichtbaar voor alle geregistreerde AUR-gebruikers. Als u uw e-mailadres wél verbergt, dan is het alleen zichtbaar voor Arch Linux-medewerkers." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Tweede e-mailadres" #: 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 "U kunt desgewenst een tweede e-mailadres opgeven. Zo kunt u uw account herstellen als u geen toegang meer heeft tot uw primaire e-mailaccount." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Er is een wachtwoordherstelsleutel verstuurd naar uw e-mailadres en uw tweede e-mailadres." #: 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 "Uw tweede e-mailadres is alleen zichtbaar voor Arch Linux-medewerkers, ongeacht de instelling ‘%s’." #: template/account_edit_form.php msgid "Language" @@ -1331,17 +1387,17 @@ msgstr "Taal" #: template/account_edit_form.php msgid "Timezone" -msgstr "" +msgstr "Tijdzone" #: template/account_edit_form.php msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Als u uw wachtwoord wilt wijzigen, voer dan een nieuw wachtwoord in en bevestig het door het nogmaals in te voeren." #: template/account_edit_form.php msgid "Re-type password" -msgstr "Voer wachtwoord opnieuw in" +msgstr "Wachtwoord bevestigen" #: template/account_edit_form.php msgid "" @@ -1351,46 +1407,46 @@ msgstr "De volgende informatie is alleen verplicht als u pakketten aan de Arch U #: template/account_edit_form.php msgid "SSH Public Key" -msgstr "SSH Publieke Sleutel" +msgstr "Publieke ssh-sleutel" #: template/account_edit_form.php msgid "Notification settings" -msgstr "" +msgstr "Meldingsinstellingen" #: template/account_edit_form.php msgid "Notify of new comments" -msgstr "Notificatie bij nieuwe comment" +msgstr "Melding bij nieuwe opmerkingen" #: template/account_edit_form.php msgid "Notify of package updates" -msgstr "" +msgstr "Melding bij pakketupdates" #: template/account_edit_form.php msgid "Notify of ownership changes" -msgstr "" +msgstr "Melding bij nieuwe pakketeigenaar" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Voer uw huidige wachtwoord in om uw profiel op te slaan:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Huidig wachtwoord" #: 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 "Geef de uitvoer van de volgende opdracht op om automatische accountaanmaak te voorkomen:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Antwoord" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php msgid "Update" -msgstr "Update" +msgstr "Bijwerken" #: template/account_edit_form.php msgid "Create" @@ -1398,15 +1454,15 @@ msgstr "Aanmaken" #: template/account_edit_form.php template/search_accounts_form.php msgid "Reset" -msgstr "Reset" +msgstr "Standaardwaarden" #: template/account_search_results.php msgid "No results matched your search criteria." -msgstr "Geen resultaten die voldoen aan je zoekcriteria." +msgstr "Er zijn geen zoekresultaten die overeenkomen met uw zoekcriteria." #: template/account_search_results.php msgid "Edit Account" -msgstr "Bewerk account" +msgstr "Account bewerken" #: template/account_search_results.php msgid "Suspended" @@ -1426,13 +1482,13 @@ msgstr "Meer" #: template/account_search_results.php msgid "No more results to display." -msgstr "Geen andere resultaten gevonden " +msgstr "Geen verdere resultaten gevonden." #: template/comaintainers_form.php #, php-format msgid "" "Use this form to add co-maintainers for %s%s%s (one user name per line):" -msgstr "Gebruik dit formulier om mede-onderhouders voor %s%s%s toe te voegen (een gebruikersnaam per regel):" +msgstr "Gebruik dit formulier om mede-onderhouders toe te voegen aan ‘%s%s%s’ (één gebruikersnaam per regel):" #: template/comaintainers_form.php msgid "Users" @@ -1440,88 +1496,88 @@ msgstr "Gebruikers" #: template/comaintainers_form.php template/pkg_comment_form.php msgid "Save" -msgstr "Bewaren" +msgstr "Opslaan" #: template/flag_comment.php #, php-format msgid "Flagged Out-of-Date Comment: %s" -msgstr "" +msgstr "Gemarkeerde verouderde opmerking: %s" #: template/flag_comment.php #, php-format msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" -msgstr "" +msgstr "%s%s%s heeft %s%s%s gemarkeerd als verouderd op %s%s%s omwille van de volgende reden:" #: template/flag_comment.php #, php-format msgid "%s%s%s is not flagged out-of-date." -msgstr "" +msgstr "%s%s%s is niet gemarkeerd als verouderd." #: template/flag_comment.php msgid "Return to Details" -msgstr "" +msgstr "Terug naar informatiepagina" #: template/footer.php #, php-format msgid "Copyright %s 2004-%d aurweb Development Team." -msgstr "" +msgstr "Copyright %s 2004-%d aurweb-ontwikkelaarsteam." #: template/header.php msgid " My Account" -msgstr "Mijn Account" +msgstr "Mijn account" #: template/pkgbase_actions.php msgid "Package Actions" -msgstr "Pakket Acties" +msgstr "Pakketacties" #: template/pkgbase_actions.php msgid "View PKGBUILD" -msgstr "Toon PKGBUILD" +msgstr "PKGBUILD tonen" #: template/pkgbase_actions.php msgid "View Changes" -msgstr "Bekijk Wijzigingen" +msgstr "Wijzigingen bekijken" #: template/pkgbase_actions.php msgid "Download snapshot" -msgstr "Download momentopname" +msgstr "Momentopname downloaden" #: template/pkgbase_actions.php msgid "Search wiki" -msgstr "Doorzoek wiki" +msgstr "Wiki doorzoeken" #: template/pkgbase_actions.php #, php-format msgid "Flagged out-of-date (%s)" -msgstr "" +msgstr "Gemarkeerd als verouderd (%s)" #: template/pkgbase_actions.php msgid "Flag package out-of-date" -msgstr "Markeer pakket als verouderd" +msgstr "Markeren als verouderd" #: template/pkgbase_actions.php msgid "Unflag package" -msgstr "Verwijder markering" +msgstr "Markering verwijderen" #: template/pkgbase_actions.php msgid "Remove vote" -msgstr "Verwijder stem" +msgstr "Stem intrekken" #: template/pkgbase_actions.php msgid "Vote for this package" -msgstr "Stem voor dit pakket" +msgstr "Stemmen op dit pakket" #: template/pkgbase_actions.php scripts/notify.py msgid "Disable notifications" -msgstr "Schakel notificaties uit" +msgstr "Meldingen uitschakelen" #: template/pkgbase_actions.php template/pkg_comment_form.php msgid "Enable notifications" -msgstr "" +msgstr "Meldingen inschakelen" #: template/pkgbase_actions.php msgid "Manage Co-Maintainers" -msgstr "Beheer mede-onderhouders" +msgstr "Mede-onderhouders beheren" #: template/pkgbase_actions.php #, php-format @@ -1532,28 +1588,28 @@ msgstr[1] "%d verzoeken in wachtrij" #: template/pkgbase_actions.php msgid "Adopt Package" -msgstr "Adopteer Pakket" +msgstr "Eigenaar worden" #: template/pkgbase_details.php msgid "Package Base Details" -msgstr "Details van Basispakket" +msgstr "Details van basispakket" #: template/pkgbase_details.php template/pkg_details.php msgid "Git Clone URL" -msgstr "Git Clone URL" +msgstr "Git clone-url" #: template/pkgbase_details.php template/pkg_details.php msgid "read-only" -msgstr "" +msgstr "alleen-lezen" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "klik om te kopiëren" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" -msgstr "Sleutelwoorden" +msgstr "Trefwoorden" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1563,11 +1619,11 @@ msgstr "Inzender" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php msgid "Maintainer" -msgstr "Beheerder" +msgstr "Eigenaar" #: template/pkgbase_details.php template/pkg_details.php msgid "Last Packager" -msgstr "Laatste Packager" +msgstr "Recentste pakketbouwer" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php @@ -1585,94 +1641,94 @@ msgstr "Toegevoegd" #: template/pkgbase_details.php template/pkg_details.php msgid "Last Updated" -msgstr "Laatste Update" +msgstr "Recentste update" #: template/pkg_comment_box.php #, php-format msgid "Edit comment for: %s" -msgstr "" +msgstr "Opmerking bewerken van: %s" #: template/pkg_comment_box.php template/pkg_comment_form.php msgid "Add Comment" -msgstr "Voeg Comment Toe" +msgstr "Opmerking toevoegen" #: template/pkg_comment_form.php msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Git-commitidentificaties die verwijzen naar commits in de AUR-bron en url's, worden automatisch omgezet naar links." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "%sMarkdown-syntax%s wordt gedeeltelijk ondersteund." #: template/pkg_comments.php msgid "Pinned Comments" -msgstr "" +msgstr "Vastgemaakte opmerkingen" #: template/pkg_comments.php msgid "Latest Comments" -msgstr "Nieuwste Comments" +msgstr "Nieuwste opmerkingen" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Opmerkingen bij" #: template/pkg_comments.php #, php-format msgid "%s commented on %s" -msgstr "" +msgstr "%s heeft een opmerking geplaatst bij %s" #: template/pkg_comments.php #, php-format msgid "Anonymous comment on %s" -msgstr "" +msgstr "Anonieme opmerking bij %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Opmerking geplaatst bij %s op %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" -msgstr "" +msgstr "verwijderd op %s door %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s" -msgstr "" +msgstr "verwijderd op %s" #: template/pkg_comments.php #, php-format msgid "edited on %s by %s" -msgstr "" +msgstr "bewerkt op %s door %s" #: template/pkg_comments.php #, php-format msgid "edited on %s" -msgstr "" +msgstr "bewerkt op %s" #: template/pkg_comments.php msgid "Undelete comment" -msgstr "" +msgstr "Opmerking terugplaatsen" #: template/pkg_comments.php msgid "Delete comment" -msgstr "Verwijder opmerking" +msgstr "Opmerking verwijderen" #: template/pkg_comments.php msgid "Pin comment" -msgstr "" +msgstr "Opmerking vastmaken" #: template/pkg_comments.php msgid "Unpin comment" -msgstr "" +msgstr "Opmerking losmaken" #: template/pkg_details.php msgid "Package Details" -msgstr "Pakketdetails" +msgstr "Pakketinformatie" #: template/pkg_details.php template/pkg_search_form.php msgid "Package Base" @@ -1684,11 +1740,11 @@ msgstr "Omschrijving" #: template/pkg_details.php msgid "Upstream URL" -msgstr "Upstream URL" +msgstr "Upstream-url" #: template/pkg_details.php msgid "Visit the website for" -msgstr "Bezoek de website voor" +msgstr "Ga naar de website van" #: template/pkg_details.php msgid "Licenses" @@ -1725,13 +1781,13 @@ msgstr "Bronnen" #: template/pkgreq_close_form.php #, php-format msgid "Use this form to close the request for package base %s%s%s." -msgstr "Gebruik dit formulier om het verzoek voor basispakket %s%s%s te sluiten." +msgstr "Gebruik dit formulier om het verzoek aangaande het basispakket ‘%s%s%s’ te sluiten." #: template/pkgreq_close_form.php msgid "" "The comments field can be left empty. However, it is highly recommended to " "add a comment when rejecting a request." -msgstr "Het commentaarveld mag leeg blijven. Echter, het wordt sterk aanbevolen om een afwijzing van commentaar te voorzien." +msgstr "Het commentaarveld mag worden leeggelaten, maar het wordt sterk aanbevolen om een afwijzing van commentaar te voorzien." #: template/pkgreq_close_form.php msgid "Reason" @@ -1740,7 +1796,7 @@ msgstr "Reden" #: template/pkgreq_close_form.php template/pkgreq_results.php #: template/tu_details.php msgid "Accepted" -msgstr "Geaccepteerd" +msgstr "Goedgekeurd" #: template/pkgreq_close_form.php template/pkgreq_results.php #: template/tu_details.php @@ -1752,11 +1808,11 @@ msgstr "Afgewezen" msgid "" "Use this form to file a request against package base %s%s%s which includes " "the following packages:" -msgstr "Gebruik dit formulier om een verzoek in te dienen voor basispakket %s%s%s welke de volgende pakketten omvat:" +msgstr "Gebruik dit formulier om een verzoek in te dienen voor het basispakket ‘%s%s%s’, wat de volgende pakketten bevat:" #: template/pkgreq_form.php msgid "Request type" -msgstr "Verzoektype" +msgstr "Soort verzoek" #: template/pkgreq_form.php msgid "Deletion" @@ -1764,18 +1820,18 @@ msgstr "Verwijdering" #: template/pkgreq_form.php msgid "Orphan" -msgstr "Wees" +msgstr "Onbeheerd" #: template/pkgreq_form.php template/pkg_search_results.php msgid "Merge into" -msgstr "Voeg samen met" +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 " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "" +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." #: template/pkgreq_form.php msgid "" @@ -1783,7 +1839,7 @@ msgid "" "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 "" +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." #: template/pkgreq_form.php msgid "" @@ -1791,11 +1847,11 @@ msgid "" "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 "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." #: template/pkgreq_results.php msgid "No requests matched your search criteria." -msgstr "" +msgstr "Er zijn geen verzoeken die overeenkomen met uw zoekcriteria." #: template/pkgreq_results.php #, php-format @@ -1825,8 +1881,8 @@ msgstr "Datum" #, php-format msgid "~%d day left" msgid_plural "~%d days left" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "~%d dag resterend" +msgstr[1] "~%d dagen resterend" #: template/pkgreq_results.php #, php-format @@ -1841,7 +1897,7 @@ msgstr "<1 uur resterend" #: template/pkgreq_results.php msgid "Accept" -msgstr "Accepteer" +msgstr "Goedkeuren" #: template/pkgreq_results.php msgid "Locked" @@ -1849,11 +1905,11 @@ msgstr "Vergrendeld" #: template/pkgreq_results.php msgid "Close" -msgstr "Sluit" +msgstr "Sluiten" #: template/pkgreq_results.php msgid "Pending" -msgstr "" +msgstr "In behandeling" #: template/pkgreq_results.php msgid "Closed" @@ -1869,19 +1925,19 @@ msgstr "Alleen de naam" #: template/pkg_search_form.php msgid "Exact Name" -msgstr "Exacte Naam" +msgstr "Exacte naam" #: template/pkg_search_form.php msgid "Exact Package Base" -msgstr "Exact Basispakket" +msgstr "Exact basispakket" #: template/pkg_search_form.php msgid "Co-maintainer" -msgstr "" +msgstr "Mede-onderhouder" #: template/pkg_search_form.php msgid "Maintainer, Co-maintainer" -msgstr "" +msgstr "Eigenaar, Mede-onderhouder" #: template/pkg_search_form.php msgid "All" @@ -1906,7 +1962,7 @@ msgstr "Gestemd" #: template/pkg_search_form.php msgid "Last modified" -msgstr "" +msgstr "Laatst gewijzigd" #: template/pkg_search_form.php msgid "Ascending" @@ -1922,7 +1978,7 @@ msgstr "Voer zoekcriteria in" #: template/pkg_search_form.php msgid "Search by" -msgstr "Zoek op" +msgstr "Zoeken op" #: template/pkg_search_form.php template/stats/user_table.php msgid "Out of Date" @@ -1930,7 +1986,7 @@ msgstr "Verouderd" #: template/pkg_search_form.php template/search_accounts_form.php msgid "Sort by" -msgstr "Sorteer Op" +msgstr "Sorteren op" #: template/pkg_search_form.php msgid "Sort order" @@ -1946,15 +2002,15 @@ msgstr "Ga" #: template/pkg_search_form.php msgid "Orphans" -msgstr "Wezen" +msgstr "Onbeheerd" #: template/pkg_search_results.php msgid "Error retrieving package list." -msgstr "Fout bij het ophalen van pakkettenlijst" +msgstr "De pakketlijst kan niet worden opgehaald." #: template/pkg_search_results.php msgid "No packages matched your search criteria." -msgstr "Geen pakketen gevonden die aan uw zoekcriteria voldoen" +msgstr "Er zijn geen pakketten die overeenkomen met uw zoekcriteria." #: template/pkg_search_results.php #, php-format @@ -1972,7 +2028,7 @@ msgstr "Versie" 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 "De populariteit wordt vastgesteld op basis van het totaal aantal stemmen plus een factor %.2f per dag sinds de aanmaak." #: template/pkg_search_results.php template/tu_details.php #: template/tu_list.php @@ -1981,7 +2037,7 @@ msgstr "Ja" #: template/pkg_search_results.php msgid "orphan" -msgstr "wees" +msgstr "onbeheerd" #: template/pkg_search_results.php msgid "Actions" @@ -1989,23 +2045,23 @@ msgstr "Acties" #: template/pkg_search_results.php msgid "Unflag Out-of-date" -msgstr "Hef Out-of-Date markering op" +msgstr "Demarkeren als verouderd" #: template/pkg_search_results.php msgid "Adopt Packages" -msgstr "Adopteer Pakketten" +msgstr "Eigenaar worden" #: template/pkg_search_results.php msgid "Disown Packages" -msgstr "Onteigen Pakketten" +msgstr "Pakketten onteigenen" #: template/pkg_search_results.php msgid "Delete Packages" -msgstr "Verwijder pakketten" +msgstr "Pakketten verwijderen" #: template/pkg_search_results.php msgid "Confirm" -msgstr "Bevestig" +msgstr "Bevestigen" #: template/search_accounts_form.php msgid "Any type" @@ -2013,7 +2069,7 @@ msgstr "Elk type" #: template/search_accounts_form.php msgid "Search" -msgstr "Zoek" +msgstr "Zoeken" #: template/stats/general_stats_table.php msgid "Statistics" @@ -2021,27 +2077,27 @@ msgstr "Statistieken" #: template/stats/general_stats_table.php msgid "Orphan Packages" -msgstr "Weespakketten" +msgstr "Onteigend" #: template/stats/general_stats_table.php msgid "Packages added in the past 7 days" -msgstr "Pakketten toegevoegd in de laatste 7 dagen" +msgstr "Toegevoegd in de afgelopen 7 dagen" #: template/stats/general_stats_table.php msgid "Packages updated in the past 7 days" -msgstr "Pakketten geüpdatet in de laatste 7 dagen" +msgstr "Bijgewerkt in de afgelopen 7 dagen" #: template/stats/general_stats_table.php msgid "Packages updated in the past year" -msgstr "Pakketen geüpdatet in het laatste jaar" +msgstr "Bijgewerkt in het afgelopen jaar" #: template/stats/general_stats_table.php msgid "Packages never updated" -msgstr "Nooit geüpdate pakketten" +msgstr "Nooit bijgewerkt" #: template/stats/general_stats_table.php msgid "Registered Users" -msgstr "Geregistreerde Gebruikers" +msgstr "Geregistreerde gebruikers" #: template/stats/general_stats_table.php msgid "Trusted Users" @@ -2053,7 +2109,7 @@ msgstr "Recente updates" #: template/stats/updates_table.php msgid "more" -msgstr "" +msgstr "meer" #: template/stats/user_table.php msgid "My Statistics" @@ -2065,12 +2121,12 @@ msgstr "Voorsteldetails" #: template/tu_details.php msgid "This vote is still running." -msgstr "Dit voorstel is nog bezig" +msgstr "De stemming loopt nog." #: template/tu_details.php #, php-format msgid "Submitted: %s by %s" -msgstr "Opgestuurd: %s door %s" +msgstr "Ingediend: %s, door %s" #: template/tu_details.php template/tu_list.php msgid "End" @@ -2086,7 +2142,7 @@ msgstr "Nee" #: template/tu_details.php msgid "Abstain" -msgstr "Onthouden" +msgstr "Blanco stemmen" #: template/tu_details.php msgid "Total" @@ -2094,15 +2150,15 @@ msgstr "Totaal" #: template/tu_details.php msgid "Participation" -msgstr "Participatie" +msgstr "Deelname" #: template/tu_last_votes_list.php msgid "Last Votes by TU" -msgstr "Laatste stemmen door TU" +msgstr "Recentste stemmen van TU" #: template/tu_last_votes_list.php msgid "Last vote" -msgstr "Laatste stem" +msgstr "Recentste stem" #: template/tu_last_votes_list.php template/tu_list.php msgid "No results found." @@ -2118,7 +2174,7 @@ msgstr "Terug" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "" +msgstr "AUR-wachtwoordherstel" #: scripts/notify.py #, python-brace-format @@ -2126,90 +2182,90 @@ 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 "Er is een verzoek ingediend om het wachtwoord te herstellen van het account ‘{user}’, wat geregistreerd staat op uw e-mailadres. Als u uw wachtwoord wilt herstellen, volg dan onderstaande link ([1]). Als u dat niet wilt, kunt u deze e-mail beschouwen als niet-verzonden." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "Welkom bij de Arch User Respository" #: 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 "Welkom bij de Arch User Repository! Als u een nieuw wachtwoord voor uw account wilt instellen, klik dan op de link ([1]) hieronder. Als de link niet werkt, kopieer en plak deze dan in uw browser." #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "AUR-opmerking bij {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "{user} ([1]) heeft een opmerking geplaatst bij {pkgbase} ([2]): " #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Als u geen meldingen aangaande dit pakket meer wilt ontvangen, ga dan naar de pakketpagina ([2]) en klik op ‘{label}’." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "AUR-pakketupdate: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} ([1]) heeft een commit toegevoegd aan {pkgbase} ([2]): " #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "AUR-verouderingsmelding van {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "Uw pakket genaamd ‘{pkgbase} ([1])’ is als verouderd gemarkeerd door {user} [2]:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "AUR-eigenaarsmelding van {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "{user} [2] is de nieuwe eigenaar van het pakket ‘{pkgbase}’ [1]." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "{pkgbase} [1] is onteigend door {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "AUR-mede-eigenaarsmelding van {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "U bent toegevoegd aan de lijst met onderhouder van {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "U bent verwijderd van de lijst met onderhouder van {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "AUR-pakket verwijderd: {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2218,7 +2274,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] heeft {old} [2] samengevoegd met {new} [3].\n\n--\nAls u geen meldingen aangaande dit pakket meer wilt ontvangen, ga dan naar de pakketpagina ([2]) en klik op ‘{label}’." #: scripts/notify.py #, python-brace-format @@ -2226,16 +2282,61 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] heeft {pkgbase} [2] verwijderd.\n\n\nU ontvangt geen meldingen meer aangaande dit pakket." #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "TU-stemherinnering aangaande het voorstel ‘{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 "Vergeet niet uw stem uit te brengen op het voorstel ‘{id}’ [1]. De stemming sluit over minder dan 48 uur." + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Ongeldig accounttype." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "U bent niet bevoegd om accounttypes te wijzigen." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +msgstr "U bent niet bevoegd om het accounttype van deze gebruiker te wijzigen in ‘%s’." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "Er zijn geen goed te keuren onteigeningsverzoeken omtrent %s." + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "Interne serverfout" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +msgstr "Er is een kritieke fout opgetreden." + +#: 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 "De details zijn vastgelegd en worden nagekeken door de beheerder. Onze excuses voor het eventuele ontstane ongemak." + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "AUR-serverfout" + +#: 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 "" diff --git a/po/pl.po b/po/pl.po index 56e9f6bb..4856f22b 100644 --- a/po/pl.po +++ b/po/pl.po @@ -10,18 +10,19 @@ # Chris Warrick, 2012 # Kwpolska , 2011 # Lukas Fleischer , 2011 -# Marcin Mikołajczak , 2017 +# marcin mikołajczak , 2017 # Michal T , 2016 # Nuc1eoN , 2014 # Piotr Strębski , 2017-2018 # 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" "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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -86,7 +87,7 @@ msgstr "Nie masz uprawnień do edycji tego konta." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Błędne hasło." #: html/account.php msgid "Use this form to search existing accounts." @@ -482,6 +483,12 @@ msgid "" "checkbox." msgstr "Wybrane pakiety nie zostały porzucone, sprawdź pole wyboru potwierdzenia." +#: 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 "Nie można znaleźć pakietu do scalenia głosów i komentarzy." @@ -580,6 +587,14 @@ msgstr "Oznacz komentarz" msgid "Flag Package Out-Of-Date" msgstr "Oznacz pakiet jako nieaktualny" +#: 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 "" @@ -876,6 +891,10 @@ msgstr "Formularz logowania jest obecnie wyłączony dla Twojego adresu IP, najp msgid "Account suspended" msgstr "Konto zawieszone" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -961,6 +980,30 @@ msgstr "Błąd podczas pobierania informacji o pakiecie." msgid "Package details could not be found." msgstr "Nie odnaleziono informacji o pakiecie." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Musisz być zalogowany aby móc oznaczyć pakiety." @@ -997,6 +1040,10 @@ msgstr "Nie masz uprawnień do usuwania pakietów." msgid "You did not select any packages to delete." msgstr "Nie wybrałeś żadnych pakietów do usunięcia." +#: 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 "Wybrane pakiety zostały usunięte." @@ -1005,10 +1052,18 @@ msgstr "Wybrane pakiety zostały usunięte." msgid "You must be logged in before you can adopt packages." msgstr "Musisz być zalogowany aby móc przejmować pakiety." +#: 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 "Musisz być zalogowany, aby móc porzucać pakiety." +#: 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 "Nie wybrałeś żadnych pakietów do przejęcia." @@ -2256,3 +2311,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/pt.po b/po/pt.po new file mode 100644 index 00000000..b2cf86b2 --- /dev/null +++ b/po/pt.po @@ -0,0 +1,2336 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +# Lukas Fleischer , 2011 +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "Contas" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "Você não está autorizado a acessar esta área." + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "Não foi possível recuperar as informações do usuário especificado." + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "Você não tem permissão para editar este conta." + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "Use este formulário para pesquisar as contas existentes." + +#: html/account.php +msgid "You must log in to view user information." +msgstr "Você precisa efetuar o login para visualizar as informações do usuário." + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "Tipo" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "Enviar" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "Inicial" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "Meus Pacotes" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +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 "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "Votar" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "Notificar" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "Retirar Notificação" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "Login" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "Logado como: %s" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "Sair" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "Senha" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "Critério de Pesquisa" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "Pacotes" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "Erro ao tentar retornar os detalhes do pacote." + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "Um campo requerido não foi informado." + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "As senhas informadas não conferem." + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "Use este formulário para criar uma conta." + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "Usuário Confiável" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "ID de usuário não encontrada" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "O endereço de email é inválido." + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "Idioma atualmente não suportado." + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "Comentário foi adicionado." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "Você deve ter efetuado o login antes de poder editar as informações do pacote." + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "ID de comentário não encontrada." + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "Erro ao retornar os detalhes do pacote." + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "Detalhes do Pacote não foram encontrados." + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "Você deve efetuar o login antes de poder marcar pacotes." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "Você não selecionou nenhum pacote para marcar." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "Os pacotes selecionados foram marcados como desatualizados." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "Você deve efetuar o login antes de poder desmarcar pacotes." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "Você não selecionou nenhum pacote para desmarcar." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "Os pacotes selecionados tiveram seus marcadores retirados." + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to delete." +msgstr "Você não selecionou nenhum pacote para excluir." + +#: 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 "Os pacotes selecionados foram excluídos." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can adopt packages." +msgstr "Você deve efetuar o login antes de poder adotar pacotes." + +#: 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 "Você deve efetuar o login antes de poder abandonar pacotes" + +#: 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 "Você não selecionou nenhum pacote para adotar." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "Você não selecionou nenhum pacote para abandonar." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "Os pacotes selecionados foram adotados." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "Os pacotes selecionados foram abandonados." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "Você deve efetuar o login antes de poder votar nos pacotes." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "Você deve efetuar o login antes de poder retirar seu voto de um pacote." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "Você não selecionou nenhum pacote para votar." + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "Seus votos foram removidos dos pacotes selecionados." + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "Os pacotes selecionados foram votados." + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "Não foi possível adicionar para a lista de notificação." + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "Você não está autorizado a apagar este comentário." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "Comentário foi excluído." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "Nome inválido: somente letras minúsculas são permitidas." + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "Nome de Usuário" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "Tipo da Conta" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "Usuário" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "Desenvolvedor" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "Endereço de Email" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "Nome Real" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "Nick de IRC" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "Status" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "Ativo" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "Desconhecida" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "Nunca" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "Visualizar os pacotes deste usuário" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "requerido" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "Idioma" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Confirme a senha" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "Atualizar" + +#: template/account_edit_form.php +msgid "Create" +msgstr "Criar" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "Resetar" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "Seu critério de pesquisa não retornou nenhum resultado." + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "Editar Conta" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "Suspenso" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "Menor" + +#: template/account_search_results.php +msgid "More" +msgstr "Mais" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "Sem mais resultados para exibir." + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "Colaborador" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "Mantenedor" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "Votos" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "Submetido pela primeira vez" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "Últimos Pacotes Atualizados" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "Excluir Comentário" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "Detalhes do Pacote" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "Descrição" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "Dependências" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "Fontes" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "Todos" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "Nome" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "Votado" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "Ascendente" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "Decrescente" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "Pesquisar por" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "Ordenar por" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "Ordem" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "Por página" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "Ir" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "Orfãos" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "Erro ao retornar a lista de pacotes." + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "Seu critério de pesquisa não retornou nenhum pacote." + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "Sim" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "orfão" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "Ações" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "Retirar marcador de Desatualizado" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "Adotar Pacotes" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "Abandonar Pacotes" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "Excluir Pacote" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "Qualquer tipo" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "Pesquisa'" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "Estatísticas" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "Usuários Registrados" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "Usuários Confiáveis" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "Atualizações Recentes" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "Minhas Estatísticas" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "Não" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/pt_BR.po b/po/pt_BR.po index 1a54b01a..c9c15d72 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -6,7 +6,7 @@ # Albino Biasutti Neto Bino , 2011 # Fábio Nogueira , 2016 # Rafael Fontenelle , 2012-2015 -# Rafael Fontenelle , 2011,2015-2018,2020 +# Rafael Fontenelle , 2011,2015-2018,2020-2022 # Rafael Fontenelle , 2011 # Sandro , 2011 # Sandro , 2011 @@ -15,8 +15,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-01-31 12:57+0000\n" -"Last-Translator: Rafael Fontenelle \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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -57,7 +57,7 @@ msgstr "Serviço indisponível" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "Sem pânico! Este site está fechado para manutenção. Voltaremos às atividades em breve." +msgstr "Sem pânico! Este site está indisponível para manutenção. Voltaremos às atividades em breve." #: html/account.php msgid "Account" @@ -89,7 +89,7 @@ msgstr "Utilize este formulário para procurar contas existentes." #: html/account.php msgid "You must log in to view user information." -msgstr "Você precisa fazer login para ver as informações do usuário." +msgstr "Você precisa se conectar para ver as informações de usuários." #: html/addvote.php template/tu_list.php msgid "Add Proposal" @@ -101,7 +101,7 @@ msgstr "Token inválido para a ação de usuário." #: html/addvote.php msgid "Username does not exist." -msgstr "Usuário não existe." +msgstr "Nome de usuário não existe." #: html/addvote.php #, php-format @@ -126,7 +126,7 @@ msgstr "Enviar uma proposta para ser votada." #: html/addvote.php msgid "Applicant/TU" -msgstr "Requerente/UC" +msgstr "Requerente/TU" #: html/addvote.php msgid "(empty if not applicable)" @@ -139,15 +139,15 @@ msgstr "Tipo" #: html/addvote.php msgid "Addition of a TU" -msgstr "Adição de um UC" +msgstr "Adição de um TU" #: html/addvote.php msgid "Removal of a TU" -msgstr "Remoção de um UC" +msgstr "Remoção de um TU" #: html/addvote.php msgid "Removal of a TU (undeclared inactivity)" -msgstr "Remoção de um UC (inatividade não declarada)" +msgstr "Remoção de um TU (inatividade não declarada)" #: html/addvote.php msgid "Amendment of Bylaws" @@ -163,7 +163,7 @@ msgstr "Enviar" #: html/comaintainers.php template/comaintainers_form.php msgid "Manage Co-maintainers" -msgstr "Gerenciar co-mantenedores" +msgstr "Gerenciar comantenedores" #: html/commentedit.php template/pkg_comments.php msgid "Edit comment" @@ -171,7 +171,7 @@ msgstr "Editar comentário" #: html/home.php template/header.php msgid "Dashboard" -msgstr "Dashboard" +msgstr "Painel de controle" #: html/home.php template/header.php msgid "Home" @@ -235,7 +235,7 @@ msgstr "Os pacotes do AUR são conteúdos produzidos por usuários. Qualquer uso #: html/home.php msgid "Learn more..." -msgstr "Aprenda mais..." +msgstr "Saiba mais..." #: html/home.php msgid "Support" @@ -260,7 +260,7 @@ msgstr "Requisição para tornar o pacote órfão" 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 "Requisite que seja desvinculado um pacote de seu mantenedor, de forma que o pacote fique órfão, quando, por exemplo, o mantenedor está inativo e o pacote foi marcado como desatualizado há muito tempo." +msgstr "Requisite que um pacote seja desvinculado de seu mantenedor, de forma que o pacote fique órfão, quando, por exemplo, o mantenedor está inativo e o pacote foi marcado como desatualizado há muito tempo." #: html/home.php msgid "Deletion Request" @@ -271,7 +271,7 @@ 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 esse pacote órfão." +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." #: html/home.php msgid "Merge Request" @@ -288,7 +288,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 user 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" @@ -316,7 +316,7 @@ 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 do Usuário Confiável acontecem no %saur-general%s. Para discussão relacionada ao desenvolvimento do AUR web, use a lista de discussão do %saur-dev%s" +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" #: html/home.php msgid "Bug Reporting" @@ -329,7 +329,7 @@ msgid "" "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 %sbug tracker%s. Use o tracker 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." +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." #: html/home.php msgid "Package Search" @@ -361,7 +361,7 @@ msgstr "Desmarcar" #: html/login.php template/header.php msgid "Login" -msgstr "Login" +msgstr "Conectar" #: html/login.php html/tos.php #, php-format @@ -374,7 +374,7 @@ msgstr "Sair" #: html/login.php msgid "Enter login credentials" -msgstr "Digite as credenciais de login" +msgstr "Digite as credenciais para se conectar" #: html/login.php msgid "User name or primary email address" @@ -396,7 +396,7 @@ msgstr "Esqueci minha senha" #, php-format msgid "" "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." -msgstr "Login via HTTP está desabilitado. Favor %sacesse via HTTPs%s caso queira fazer login." +msgstr "Acesso por HTTP está desabilitado. Favor %sacesse via HTTPs%s caso queira se conectar." #: html/packages.php template/pkg_search_form.php msgid "Search Criteria" @@ -446,7 +446,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 "Informe com sua senha:" +msgstr "Digite a sua senha:" #: html/passreset.php msgid "Confirm your new password:" @@ -477,9 +477,15 @@ msgid "" "checkbox." msgstr "Os pacotes selecionados não foram abandonados, marque a caixa de confirmação." +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "Os pacotes selecionados não foram adotados, marque a caixa de confirmação." + #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." -msgstr "Não foi possível encontrar pacote para nele fundir votos e comentários." +msgstr "Não foi possível encontrar pacote para mesclar votos e comentários nele." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot merge a package base with itself." @@ -489,7 +495,7 @@ msgstr "Não é possível mesclar um pacote base com ele mesmo" msgid "" "The selected packages have not been deleted, check the confirmation " "checkbox." -msgstr "Os pacotes selecionados não foram apagados, marque a caixa de confirmação." +msgstr "Os pacotes selecionados não foram excluídos, marque a caixa de confirmação." #: html/pkgdel.php msgid "Package Deletion" @@ -524,7 +530,7 @@ msgstr "Excluir" #: html/pkgdel.php msgid "Only Trusted Users and Developers can delete packages." -msgstr "Somente Usuários Confiáveis e Desenvolvedores podem excluir pacotes." +msgstr "Somente Trusted Users e Desenvolvedores podem excluir pacotes." #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -535,7 +541,7 @@ msgstr "Abandonar pacote" msgid "" "Use this form to disown the package base %s%s%s which includes the following" " packages: " -msgstr "Use esse formulário para abandonar o pacote base %s%s%s que incluem os seguintes pacotes:" +msgstr "Use este formulário para abandonar o pacote base %s%s%s que inclui os seguintes pacotes:" #: html/pkgdisown.php msgid "" @@ -565,7 +571,7 @@ msgstr "Abandonar" #: html/pkgdisown.php msgid "Only Trusted Users and Developers can disown packages." -msgstr "Apenas Usuários Confiáveis e Desenvolvedores podem abandonar pacotes." +msgstr "Apenas Trusted Users e Desenvolvedores podem abandonar pacotes." #: html/pkgflagcomment.php msgid "Flag Comment" @@ -575,6 +581,14 @@ msgstr "Comentário da marcação" msgid "Flag Package Out-Of-Date" msgstr "Marcar pacote desatualizado" +#: 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 "Este parece ser um pacote VCS. %sNão%s marque como desatualizado se a versão do pacote no AUR não corresponder ao commit mais recente. A marcação deste pacote só deve ser feita se as fontes forem movidas ou mudanças no PKGBUILD forem necessárias devido a mudanças recentes no upstream." + #: html/pkgflag.php #, php-format msgid "" @@ -587,7 +601,7 @@ msgstr "Use este formulário para marcar o pacote base %s%s%s e os seguintes pac msgid "" "Please do %snot%s use this form to report bugs. Use the package comments " "instead." -msgstr "Por favor, %snão%s use esse formulário para relatar erros. Para isto, use os comentários do pacote." +msgstr "Por favor, %snão%s use este formulário para relatar erros. Para isto, use os comentários do pacote." #: html/pkgflag.php msgid "" @@ -647,7 +661,7 @@ msgstr "Mesclar" #: html/pkgmerge.php msgid "Only Trusted Users and Developers can merge packages." -msgstr "Somente Usuários Confiáveis e Desenvolvedores podem mesclar pacotes." +msgstr "Somente Trusted Users e Desenvolvedores podem mesclar pacotes." #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -705,7 +719,7 @@ msgstr "Eu aceito os termos e condições acima." #: html/tu.php template/account_details.php template/header.php msgid "Trusted User" -msgstr "Usuário confiável" +msgstr "Trusted User" #: html/tu.php msgid "Could not retrieve proposal details." @@ -717,7 +731,7 @@ msgstr "A votação está encerrada para esta proposta." #: html/tu.php msgid "Only Trusted Users are allowed to vote." -msgstr "Apenas Usuários Confiáveis têm permissão para votar." +msgstr "Apenas Trusted Users têm permissão para votar." #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -725,7 +739,7 @@ msgstr "Você não pode votar em uma proposta sobre você." #: html/tu.php msgid "You've already voted for this proposal." -msgstr "Você já votou nessa proposta." +msgstr "Você já votou nesta proposta." #: html/tu.php msgid "Vote ID not valid." @@ -755,7 +769,7 @@ msgstr "Faltando ID de usuário" #: lib/acctfuncs.inc.php msgid "The username is invalid." -msgstr "O usuário é inválido." +msgstr "O nome de usuário é inválido." #: lib/acctfuncs.inc.php #, php-format @@ -768,7 +782,7 @@ msgstr "Começo e fim com uma letra ou um número" #: lib/acctfuncs.inc.php msgid "Can contain only one period, underscore or hyphen." -msgstr "Pode conter somente um ponto, traço inferior ou hífen." +msgstr "Pode conter somente um ponto, sublinhado ou hífen." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." @@ -784,7 +798,7 @@ msgstr "O endereço de e-mail reserva é inválido" #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." -msgstr "A página inicial é inválida. Por favor especificar a URL HTTP(s) completa." +msgstr "A página inicial é inválida. Por favor, especificar a URL HTTP(s) completa." #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." @@ -819,7 +833,7 @@ msgstr "O endereço, %s%s%s, já está sendo usado." #: lib/acctfuncs.inc.php #, php-format msgid "The SSH public key, %s%s%s, is already in use." -msgstr "A chave pública de SSH, %s%s%s, já está em uso." +msgstr "A chave pública de SSH, %s%s%s, já está sendo usada." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." @@ -849,7 +863,7 @@ msgstr "Uma chave de redefinição de senha foi enviada para seu endereço de e- #: lib/acctfuncs.inc.php msgid "Click on the Login link above to use your account." -msgstr "Clique no link de Login acima para usar a sua conta." +msgstr "Clique no link de Conectar acima para usar a sua conta." #: lib/acctfuncs.inc.php #, php-format @@ -859,29 +873,33 @@ msgstr "Nenhuma alteração foi feita na conta, %s%s%s." #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully modified." -msgstr "A conta %s%s%s foi modificada com sucesso." +msgstr "A conta, %s%s%s, foi modificada com sucesso." #: lib/acctfuncs.inc.php msgid "" "The login form is currently disabled for your IP address, probably due to " "sustained spam attacks. Sorry for the inconvenience." -msgstr "O formulário de login está desabilitado momento para seu endereço IP, provavelmente por causa de ataques continuados de spam. Desculpe a inconveniência." +msgstr "O formulário para se conectar está desabilitado momento para seu endereço IP, provavelmente por causa de ataques continuados de spam. Desculpe a inconveniência." #: lib/acctfuncs.inc.php msgid "Account suspended" msgstr "Conta suspensa" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "Você não tem permissão para suspender contas." + #: lib/acctfuncs.inc.php #, php-format msgid "" "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 %sPassword Reset%s page." -msgstr "Sua senha foi redefinida. Se você acabou de criar uma conta, por favor use o link do e-mail de confirmação para definir uma senha inicial. Do contrário, por favor requisite uma chave de senha na página de %sRedefinição de senha%s." +msgstr "Sua senha foi redefinida. Se você acabou de criar uma conta, por favor, use o link do e-mail de confirmação para definir uma senha inicial. Do contrário, requisite uma chave de senha na página de %sRedefinição de senha%s." #: lib/acctfuncs.inc.php msgid "Bad username or password." -msgstr "Usuário ou senha inválida." +msgstr "Nome de usuário ou senha inválida." #: lib/acctfuncs.inc.php msgid "An error occurred trying to generate a user session." @@ -889,7 +907,7 @@ msgstr "Ocorreu um erro ao tentar gerar uma sessão de usuário." #: lib/acctfuncs.inc.php msgid "Invalid e-mail and reset key combination." -msgstr "Combinação entre email e chave de redefinição é inválida" +msgstr "Combinação entre e-mail e chave de redefinição é inválida" #: lib/aur.inc.php template/pkg_details.php msgid "None" @@ -922,7 +940,7 @@ msgstr "O comentário foi adicionado." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can edit package information." -msgstr "Você tem que estar conectado para poder editar informações do pacote." +msgstr "Você deve estar conectado para poder editar informações do pacote." #: lib/pkgbasefuncs.inc.php msgid "Missing comment ID." @@ -956,17 +974,41 @@ msgstr "Erro ao obter detalhes do pacote." msgid "Package details could not be found." msgstr "Não foi possível encontrar os detalhes do pacote." +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "Cabeçalho de referenciador incorreto." + +#: aurweb/routers/packages.py +msgid "You did not select any packages to be notified about." +msgstr "Você não selecionou nenhum pacote sobre o qual deva ser notificado." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "As notificações dos pacotes selecionados foram habilitadas." + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "Você não selecionou nenhum pacote para remoção de notificação." + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "Um pacote que você selecionou não está com notificações habilitadas." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." +msgstr "As notificações dos pacotes selecionados foram removidas." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." msgstr "Você deve estar conectado para marcar os pacotes." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to flag." -msgstr "Você não selecionou nenhum pacote para marcar." +msgstr "Você selecionou nenhum pacote para marcar." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have not been flagged, please enter a comment." -msgstr "Os pacotes selecionados não foram marcados como desatualizados, por favor insira um comentário." +msgstr "Os pacotes selecionados não foram marcados como desatualizados. Por favor, insira um comentário." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been flagged out-of-date." @@ -978,7 +1020,7 @@ msgstr "Você deve estar conectado para desmarcar os pacotes." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to unflag." -msgstr "Você não selecionou nenhum pacote para desmarcar." +msgstr "Você selecionou nenhum pacote para desmarcar." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been unflagged." @@ -992,6 +1034,10 @@ msgstr "Você não tem permissão para excluir pacotes." msgid "You did not select any packages to delete." msgstr "Você selecionou nenhum pacote para excluir." +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Um dos pacotes que você selecionou não existe." + #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." msgstr "Os pacotes selecionados foram excluídos." @@ -1000,13 +1046,21 @@ msgstr "Os pacotes selecionados foram excluídos." msgid "You must be logged in before you can adopt packages." msgstr "Você deve estar conectado para adotar pacotes." +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "Você não tem permissão para adotar um dos pacotes selecionados." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." msgstr "Você deve estar conectado para abandonar pacotes." +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "Você não tem permissão para abandonar um dos pacotes selecionados." + #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." -msgstr "Você não selecionou pacote para adotar." +msgstr "Você selecionou nenhum pacote para adotar." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to disown." @@ -1118,33 +1172,33 @@ msgstr "O campo de comentário não pode estar vazio" #: lib/pkgreqfuncs.inc.php msgid "Invalid request type." -msgstr "Tipo de requisição inválida" +msgstr "Tipo de requisição inválida." #: lib/pkgreqfuncs.inc.php msgid "Added request successfully." -msgstr "Requisição adicionada com sucesso" +msgstr "Requisição adicionada com sucesso." #: lib/pkgreqfuncs.inc.php msgid "Invalid reason." -msgstr "Motivo inválido" +msgstr "Motivo inválido." #: lib/pkgreqfuncs.inc.php msgid "Only TUs and developers can close requests." -msgstr "Apenas UCs e desenvolvedores podem fechar requisições." +msgstr "Apenas TUs e desenvolvedores podem fechar requisições." #: lib/pkgreqfuncs.inc.php msgid "Request closed successfully." -msgstr "Requisição fechada com sucesso" +msgstr "Requisição fechada com sucesso." #: template/account_delete.php #, php-format msgid "You can use this form to permanently delete the AUR account %s." -msgstr "Você pode usar esse formulário para excluir permanentemente a conta %s do AUR." +msgstr "Você pode usar este formulário para excluir permanentemente a conta %s do AUR." #: template/account_delete.php #, php-format msgid "%sWARNING%s: This action cannot be undone." -msgstr "%sAVISO%s: Essa ação não pode ser desfeita." +msgstr "%sAVISO%s: Esta ação não pode ser desfeita." #: template/account_delete.php msgid "Confirm deletion" @@ -1153,7 +1207,7 @@ msgstr "Confirmar exclusão" #: template/account_details.php template/account_edit_form.php #: template/account_search_results.php template/search_accounts_form.php msgid "Username" -msgstr "Usuário" +msgstr "Nome de usuário" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1173,7 +1227,7 @@ msgstr "Desenvolvedor" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Trusted User & Developer" -msgstr "Usuário Confiável & Desenvolvedor" +msgstr "Trusted User & Desenvolvedor" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1228,7 +1282,7 @@ msgstr "desconhecido" #: template/account_details.php msgid "Last Login" -msgstr "Último login" +msgstr "Último acesso" #: template/account_details.php msgid "Never" @@ -1240,7 +1294,7 @@ msgstr "Visualizar pacotes deste usuário" #: template/account_details.php msgid "Edit this user's account" -msgstr "Edite a conta desse usuário" +msgstr "Edite a conta deste usuário" #: template/account_details.php msgid "List this user's comments" @@ -1269,7 +1323,7 @@ msgstr "obrigatório" msgid "" "Your user name is the name you will use to login. It is visible to the " "general public, even if your account is inactive." -msgstr "Seu nome de usuário é o nome que você vai usar para se autenticar. É visível para o público geral, mesmo se sua contas estiver inativa." +msgstr "Seu nome de usuário é o nome que você vai usar para se autenticar. É visível para o público geral, mesmo se sua conta estiver inativa." #: template/account_edit_form.php template/search_accounts_form.php msgid "Normal user" @@ -1349,11 +1403,11 @@ msgstr "Re-digite a senha" msgid "" "The following information is only required if you want to submit packages to" " the Arch User Repository." -msgstr "A informação a seguir é necessária apenas se você deseja enviar pacotes para o Repositório de Usuário do Arch." +msgstr "A informação a seguir é necessária apenas se você deseja enviar pacotes para o Arch User Repository." #: template/account_edit_form.php msgid "SSH Public Key" -msgstr "Chave pública de SSH" +msgstr "Chave pública SSH" #: template/account_edit_form.php msgid "Notification settings" @@ -1529,8 +1583,8 @@ msgstr "Gerenciar comantenedores" #, php-format msgid "%d pending request" msgid_plural "%d pending requests" -msgstr[0] "%d requisição pentende" -msgstr[1] "%d requisições pentendes" +msgstr[0] "%d requisição pendente" +msgstr[1] "%d requisições pendentes" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1686,7 +1740,7 @@ msgstr "Descrição" #: template/pkg_details.php msgid "Upstream URL" -msgstr "URL" +msgstr "URL upstream" #: template/pkg_details.php msgid "Visit the website for" @@ -1727,7 +1781,7 @@ msgstr "Fontes" #: template/pkgreq_close_form.php #, php-format msgid "Use this form to close the request for package base %s%s%s." -msgstr "Use esse formulário para fechar a requisição para o pacote base %s%s%s." +msgstr "Use este formulário para fechar a requisição para o pacote base %s%s%s." #: template/pkgreq_close_form.php msgid "" @@ -1754,7 +1808,7 @@ msgstr "Rejeitado" msgid "" "Use this form to file a request against package base %s%s%s which includes " "the following packages:" -msgstr "Use esse formulário para fazer uma requisição sobre o pacote base %s%s%s que inclui os seguintes pacotes:" +msgstr "Use este formulário para fazer uma requisição sobre o pacote base %s%s%s que inclui os seguintes pacotes:" #: template/pkgreq_form.php msgid "Request type" @@ -1793,7 +1847,7 @@ msgid "" "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 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." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2047,7 +2101,7 @@ msgstr "Usuários registrados" #: template/stats/general_stats_table.php msgid "Trusted Users" -msgstr "Usuários Confiáveis" +msgstr "Trusted Users" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2100,7 +2154,7 @@ msgstr "Participação" #: template/tu_last_votes_list.php msgid "Last Votes by TU" -msgstr "Últimos votos por UC" +msgstr "Últimos votos por TU" #: template/tu_last_votes_list.php msgid "Last vote" @@ -2139,7 +2193,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 "Bem-vindo ao Arch User Repository! Para definir uma senha inicial para sua nova conta, por favor clique no link [1] abaixo. Se o link não funcionar, tente copiar e colá-lo no seu navegador." +msgstr "Bem-vindo ao Arch User Repository! Para definir uma senha inicial para sua nova conta, clique no link [1] abaixo. Se o link não funcionar, tente copiar e colá-lo no seu navegador." #: scripts/notify.py #, python-brace-format @@ -2241,3 +2295,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "Por favor, lembre-se de votar na proposta {id} [1]. O período de votação termina em menos de 48 horas." + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Tipo de conta inválido fornecido." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "Você não tem permissão para alterar tipos de contas." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +msgstr "Você não tem permissão para alterar o tipo de conta deste usuário para %s." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "Nenhum pedido para tornar órfão pendente aceitação para %s." + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "Erro Interno do Servidor" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +msgstr "Ocorreu um erro 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 "Os detalhes foram registrados e serão revisados ​​pelo postmaster posthaste. Pedimos desculpas por qualquer inconveniente que isso possa ter causado." + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "Erro do Servidor AUR" + +#: 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 "" diff --git a/po/pt_PT.po b/po/pt_PT.po index 0303993a..3518cb7b 100644 --- a/po/pt_PT.po +++ b/po/pt_PT.po @@ -14,8 +14,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -476,6 +476,12 @@ msgid "" "checkbox." msgstr "Os pacotes selecionados não foram desaprovados, verifique a caixa de confirmação." +#: 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 "Não é possível encontrar o pacote onde juntar os votos e os comentários." @@ -574,6 +580,14 @@ msgstr "Marcar comentário." msgid "Flag Package Out-Of-Date" msgstr "Marcar pacote desatualizado." +#: 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 "" @@ -870,6 +884,10 @@ msgstr "O formulário de início de sessão encontra-se inibido para pedidos do msgid "Account suspended" msgstr "Conta Suspensa" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -955,6 +973,30 @@ msgstr "Erro ao obter os detalhes do pacote." msgid "Package details could not be found." msgstr "Não foi possivel encontrar detalhes acerca do pacote." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Tem de iniciar sessão antes de poder marcar pacotes." @@ -991,6 +1033,10 @@ msgstr "Não tem permissão para eliminar pacotes." msgid "You did not select any packages to delete." msgstr "Não seleccionou nenhum pacote a apagar." +#: 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 "Os pacotes seleccionados foram apagados." @@ -999,10 +1045,18 @@ msgstr "Os pacotes seleccionados foram apagados." msgid "You must be logged in before you can adopt packages." msgstr "Tem de iniciar se sessão antes de poder adoptar pacotes." +#: 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 "Tem de iniciar se sessão antes de poder renunciar pacotes." +#: 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 "Não seleccionou nenhum pacote a adoptar." @@ -2240,3 +2294,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/ro.po b/po/ro.po index 7394ed83..fa159928 100644 --- a/po/ro.po +++ b/po/ro.po @@ -11,8 +11,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -473,6 +473,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 "Nu s-a putut găsi pachet pentru fuzionare voturi și comentarii." @@ -571,6 +577,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 "" @@ -867,6 +881,10 @@ msgstr "Formularul de autentificare a fost dezactivat pentru adresa ta IP, proba msgid "Account suspended" msgstr "Cont suspendat" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -952,6 +970,30 @@ msgstr "Eroare la preluarea detaliilor pachetului." msgid "Package details could not be found." msgstr "Detaliile pachetului nu pot fi găsite." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Trebuie să fi autentificat înainte de a putea marca pachetele." @@ -988,6 +1030,10 @@ msgstr "Nu ai permisiune pentru a șterge pachete." msgid "You did not select any packages to delete." msgstr "Nu ai selectat niciun pachet pentru a fi șters." +#: 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 "Pachetele selectate au fost șterse." @@ -996,10 +1042,18 @@ msgstr "Pachetele selectate au fost șterse." msgid "You must be logged in before you can adopt packages." msgstr "Trebuie să fi autentificat înainte de a putea adopta pachete." +#: 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 "Trebuie să te autentifici înainte de a abandona pachete." +#: 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 "Nu ai selectat niciun pachet pentru a-l adopta." @@ -2242,3 +2296,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/ru.po b/po/ru.po index 86a01cc6..75550c8c 100644 --- a/po/ru.po +++ b/po/ru.po @@ -3,9 +3,11 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: +# Alex , 2021 # Evgeniy Alekseev , 2014-2015 # Evgeniy Alekseev , 2014-2015, 2016 # Igor , 2017 +# Kevin Morris , 2021 # Kyrylo Silin , 2011 # Kyrylo Silin , 2011 # Lukas Fleischer , 2011 @@ -18,8 +20,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -80,11 +82,11 @@ 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." -msgstr "" +msgstr "Неверный пароль." #: html/account.php msgid "Use this form to search existing accounts." @@ -381,7 +383,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" @@ -445,7 +447,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:" @@ -464,11 +466,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,6 +482,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 "Не могу найти пакет для объединения голосов и комментариев." @@ -578,6 +586,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 "" @@ -874,6 +890,10 @@ msgstr "Доступ к форме входа с вашего IP адреса з 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 "" @@ -959,9 +979,33 @@ msgstr "Ошибка получения информации о пакете." msgid "Package details could not be found." msgstr "Не найдена информация о пакете." +#: 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 msgid "You must be logged in before you can flag packages." -msgstr "Вы должны войти чтобы ставить флажки на пакеты." +msgstr "Вы должны войти чтобы отмечать пакеты." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to flag." @@ -995,6 +1039,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 "Выбранные пакеты удалены." @@ -1003,10 +1051,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 "Вы не выбрали ни одного пакета для усыновления." @@ -2254,3 +2310,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/sk.po b/po/sk.po index d54a9dab..76d3d1a8 100644 --- a/po/sk.po +++ b/po/sk.po @@ -11,8 +11,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -473,6 +473,12 @@ msgid "" "checkbox." msgstr "Vybrané balíčky neboli vyvlastnené, pozrite potvrdzovacie zaškrtávacie políčko." +#: 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 "Nepodarilo sa nájsť balíček, do ktorého sa mali zlúčiť hlasy a komentáre." @@ -571,6 +577,14 @@ msgstr "Označ komentár" msgid "Flag Package Out-Of-Date" msgstr "Označ balíček ako zastaranú verziu" +#: 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 "" @@ -867,6 +881,10 @@ msgstr "Prihlasovací formulár je v súčasnosti zablokovaný pre vašu IP adre msgid "Account suspended" msgstr "Účet bol pozastavený" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -952,6 +970,30 @@ msgstr "Chyba pri získavaní informácií o balíčku." msgid "Package details could not be found." msgstr "Informácie o balíčku sa nepodarilo nájsť." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Musíte byť prihlásený ak chcete označovať balíčky." @@ -988,6 +1030,10 @@ msgstr "Nemáte práve vymazať balíčky." msgid "You did not select any packages to delete." msgstr "Nebol vybraný žiadny balíček na vymazanie." +#: 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 "Vybrané balíčky boli vymazané." @@ -996,10 +1042,18 @@ msgstr "Vybrané balíčky boli vymazané." msgid "You must be logged in before you can adopt packages." msgstr "Musíte byť prihlásený ak chcete adoptovať balíčky." +#: 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 "Musíte byť prihlásený ak chcete vyvlastniť balíčky." +#: 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 "Nebol vybraný žiadny balíček na adopciu." @@ -2247,3 +2301,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/sr.po b/po/sr.po index 24a52ab7..dae37bcd 100644 --- a/po/sr.po +++ b/po/sr.po @@ -11,8 +11,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -473,6 +473,12 @@ msgid "" "checkbox." msgstr "Izabrani paketi nisu odreknuti, pogledajte kućicu za potvrdu." +#: 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 "Ne mogu da nađem paket kako bih stopio glasove i komentare." @@ -571,6 +577,14 @@ msgstr "Označi komentar" msgid "Flag Package Out-Of-Date" msgstr "Označi paket kao zastareo" +#: 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 "" @@ -867,6 +881,10 @@ msgstr "Formular za prijavljivanje je trenutno onemogućen za vašu IP adresu, n msgid "Account suspended" msgstr "Nalog je suspendovan" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -952,6 +970,30 @@ msgstr "Greška pri dobavaljanju podataka o paketu." msgid "Package details could not be found." msgstr "Ne mogu naći podatke o paketu." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Morate se prijaviti pre označavanja paketa." @@ -988,6 +1030,10 @@ msgstr "Nemate dozvole za brisanje paketa." msgid "You did not select any packages to delete." msgstr "Niste izabrali pakete za brisanje." +#: 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 "Izbrani paketi su obrisani." @@ -996,10 +1042,18 @@ msgstr "Izbrani paketi su obrisani." msgid "You must be logged in before you can adopt packages." msgstr "Morate se prijaviti da bi usvojili pakete." +#: 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 "Morate se prijaviti da bi ste se odrekli paketa." +#: 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 "Niste izabrali paketa za usvajanje." @@ -2242,3 +2296,48 @@ 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/sr_RS.po b/po/sr_RS.po new file mode 100644 index 00000000..985ee007 --- /dev/null +++ b/po/sr_RS.po @@ -0,0 +1,2341 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +# Nikola Stojković , 2013 +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sr_RS\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "Почетна" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "Не заборави да гласаш за омиљене пакете!" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "Критеријум за претрагу" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "Пакети" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +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 +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/sv_SE.po b/po/sv_SE.po new file mode 100644 index 00000000..6d09e207 --- /dev/null +++ b/po/sv_SE.po @@ -0,0 +1,2340 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +# Johannes Löthberg , 2015-2016 +# Kim Svensson , 2011 +# Kim Svensson , 2012 +# Luna Jernberg , 2021 +# 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" +"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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv_SE\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "Sidan kunde inte hittas" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "Tyvärr, sidan du försöker nå existerar inte." + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "Notera" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "Tjänsten är otillgänglig" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "Få inte panik! Den här sidan är nere för underhåll. Vi kommer tillbaka snart." + +#: html/account.php +msgid "Account" +msgstr "Konto" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "Konton" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "Du har inte åtkomst till den här sidan." + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "Kunde inte nå information för den önskade användaren." + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "Du har inte tillåtelse att ändra det här kontot." + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "Ogiltigt lösenord." + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "Använd det här fältet för söka efter existerande konton." + +#: html/account.php +msgid "You must log in to view user information." +msgstr "Du måste var inloggad för att se användarinformation." + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "Lägg till förslag" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "Ogiltigt värde för användareåtgärd." + +#: html/addvote.php +msgid "Username does not exist." +msgstr "Användarnamnet finns inte." + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "%s har redan förslag som är aktivt." + +#: html/addvote.php +msgid "Invalid type." +msgstr "Ogiltig typ." + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "Förslag kan inte vara tomt." + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "Nytt förslag har skickats." + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "Skicka ett förslag att rösta på." + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "Sökande/TU" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "(tom ifall irrelevant)" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "Typ" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "Tillägg av en TU" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "Borttagning av en TU" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "Borttagning av en TU (odeklarerad inaktivitet)" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "Andringar av stadgar" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "Förslag" + +#: html/addvote.php +msgid "Submit" +msgstr "Skicka" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "Hantera sam-ansvariga" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "Redigera kommentar" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "Hem" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "Mina flaggade paket" + +#: html/home.php +msgid "My Requests" +msgstr "Mina Förfrågningar" + +#: html/home.php +msgid "My Packages" +msgstr "Mina paket" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "Sök efter paket jag underhåller" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "Sam-ansvariga paket" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "Välkommen till AUR! Var god och läs %sAUR User Guidelines%s och %sAUR TU Guidelines%s för mer information" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "Bidragna PKGBUILDer %småste%s följa %sArch Packaging Standards%s, annars kommer de bli borttagna!" + +#: html/home.php +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]." + +#: html/home.php +msgid "DISCLAIMER" +msgstr "ANSVARSFRISKRIVNING" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "AUR paket är användarproducerat innehåll. All användning av de tillhandahållna filerna görs på egen risk." + +#: html/home.php +msgid "Learn more..." +msgstr "Lär dig mer..." + +#: html/home.php +msgid "Support" +msgstr "Hjälp" + +#: html/home.php +msgid "Package Requests" +msgstr "Paket förfrågningar" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "Det finns tre typer av förfrågningar som kan öppnas i boxen %sPaket hanteringar%s på paket detaljer sidan:" + +#: html/home.php +msgid "Orphan Request" +msgstr "Härrelösnings förfrågning" + +#: 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 "Öppna en förfrågan om att göra ett paket herrelöst, till exempel när den ansvarige är inaktiv och paketet har varit flaggat utdaterat en lång tid." + +#: html/home.php +msgid "Deletion Request" +msgstr "Raderings förfrågan" + +#: 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." +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" + +#: html/home.php +msgid "Merge Request" +msgstr "Ihopslags förfrågning" + +#: html/home.php +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 "Begär att ett paket ska bli ihopslaget med ett annat. Kan användas när ett paket behöver bli omdöpt, eller ersatt av ett delat paket." + +#: 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 "Om du vill diskutera en förfrågning kan du använda %saur-requests%s maillistan. Men snälla använd inte den listan för att öppna nya förfrågningar." + +#: html/home.php +msgid "Submitting Packages" +msgstr "Ladda up paket" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "Git över SSH används nu för att ladda up paket till AUR. Se %sSubmitting packages%s sectionen av Arch User Repository artikeln på ArchWiki för mer information." + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "De följande SSH fingeravtryck används för AUR:" + +#: html/home.php +msgid "Discussion" +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." + +#: html/home.php +msgid "Bug Reporting" +msgstr "Bugg Rapportering" + +#: html/home.php +#, php-format +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." + +#: html/home.php +msgid "Package Search" +msgstr "Sök Paket" + +#: html/index.php +msgid "Adopt" +msgstr "Adoptera" + +#: html/index.php +msgid "Vote" +msgstr "Rösta" + +#: html/index.php +msgid "UnVote" +msgstr "Ångra Röst" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "Meddela" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "Meddela inte" + +#: html/index.php +msgid "UnFlag" +msgstr "Flagga inte" + +#: html/login.php template/header.php +msgid "Login" +msgstr "Logga in" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "Inloggad som: %s" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "Logga ut" + +#: html/login.php +msgid "Enter login credentials" +msgstr "Ange inloggningsuppgifter" + +#: html/login.php +msgid "User name or primary email address" +msgstr "Användarnamn eller primär epostadress" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "Lösenord" + +#: html/login.php +msgid "Remember me" +msgstr "Kom ihåg mig" + +#: html/login.php +msgid "Forgot Password" +msgstr "Glömt lösenordet" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "HTTP inloggning ej tillgänglig. Byt %sswtich till HTTPS%s om du vill logga in." + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "Sökkriterier" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "Paket" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "Fel vid hämtning av paketdetaljer." + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "Ett nödvändigt fält saknas." + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "Lösenorden stämmer inte överens." + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "Ditt lösenord måste vara åtminstone %s karaktärer." + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "Ogiltig e-postadress." + +#: html/passreset.php +msgid "Password Reset" +msgstr "Återställ Lösenord" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "Kolla din e-mailadress efter en bekräftelselänk." + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "Återställningen av ditt lösenord lyckades." + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "Bekräfta ditt användarnamn eller primär e-postadress:" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "Ange ett nytt lösenord:" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "Bekfräfta ditt nya lösenord:" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "Fortsätt" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "Ange ditt användarnamn eller primär e-postadress:" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "Paket baser" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"checkbox." +msgstr "De valda paketen har inte gjorts herrelösa, kryssa i konfirmationsrutan." + +#: 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 "Kan inte hitta paket att slå ihop röster och kommentarer i." + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "Kan inte slå ihop en paket bas med sig självt." + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "De valda paketen har inte blivit raderade, kryssa i konfirmationsrutan." + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "Paket radering" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "Radera paket" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "Använd det här formuläret för att radera paket basen %s%s%s och de följande paketen från AUR:" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "Radering av ett paket är permanent." + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "Kryssa i kryssrutan för att konfirmera handlingen." + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "Konfirmera paket radering" + +#: html/pkgdel.php template/account_delete.php +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." + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "Gör paket herrelös" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "Använd det här formuläret för att göra paket basen %s%s%s och de följande paketen härrelösa i AUR:" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "Genom att kryssa i kryssrutan konfirmerar du att du vill överföra ansvaret för detta paket till %s%s%s." + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "Genom att kryssa i kryssrutan konfirmerar du att du vill göra paketet herrelöst." + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "Konfirmera för att göra paketet herrelöst" + +#: html/pkgdisown.php +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." + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "Flagga Kommentar" + +#: html/pkgflag.php +msgid "Flag Package Out-Of-Date" +msgstr "Flagga Paketet Gammalt" + +#: 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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "Använd det här formuläret för att flagga paketbasen %s%s%s och de följande paketen som gammla: " + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "Var snäll och använd %sinte%s detta formulär för att raportera buggar. Andvänd paketkommentarerna istället." + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "Skriv in detaljer om varför paketet är gammalt nedan, helst med länkar till den nya releasen." + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "Kommentarer" + +#: html/pkgflag.php +msgid "Flag" +msgstr "Flagga" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "Bara registrerade användare kan flagga paket som gammla." + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "Paket ihopsammanslagning" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "Slå samman paket" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "Använd det här formuläret för att slå ihop paket basen %s%s%s med ett annat paket." + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "De följande paketen kommer att bli raderade:" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "När paketet har blivit ihopsammanslaget kan det inte ogöras." + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "Skriv in paket namnet du viss slå ihop detta paket med." + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "Slå ihop in i:" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "Konfirmera ihopslagning" + +#: html/pkgmerge.php template/pkgreq_form.php +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." + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "Skicka In Förfrågning" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "Stäng förfrågning" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "Första" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "Förra" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "Nästa" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "Sista" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "Förfrågningar" + +#: html/register.php template/header.php +msgid "Register" +msgstr "Registrera" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "Använd det här fältet för att skapa ett konto." + +#: html/tos.php +msgid "Terms of Service" +msgstr "Användarvillkor" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "Följande dokument har uppdaterats. Vänligen granska dem noggrant:" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "revision %d" + +#: html/tos.php +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" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "Kunde inte hämta förslagsdetaljer." + +#: html/tu.php +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." + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "Du kan inte rösta för ett förslag om dig." + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "Du har redan röstat för detta förslag." + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "Omröstnings IDt är inte giltigt" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "Nuvaranda omröstningar" + +#: html/tu.php +msgid "Past Votes" +msgstr "Tidigare omröstningar" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "Röstare" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "Konto registrering har blivit avstängt för din IP, troligen på grund av spam attacker. Vi ber om ursäkt för besväret." + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "Användar ID fattas" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "Användarnamnet är ogiltigt." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "Det måste vara mellan %s och %s karaktärer långt" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "Starta och sluta med en bokstav eller nummer" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "Kan bara innehålla en punkt, understreck, eller bindestreck." + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "Vänligen bekräfta ditt nya lösenord." + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "Email adressen är ogiltigt." + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "Backup-e-postadressen är ogiltig." + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "Hemsidan är ogiltig, ange den fullständiga HTTP(s) URL." + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "Fingeravtrycket för PGP nyckeln är ogiltigt." + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "Den publika SSH nyckeln är ogiltig." + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "Kan inte öka kontots tillgång." + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "Språket stöds för nuvarande inte." + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "Tidszon stöds för nuvarande inte." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "Användarnamnet %s%s%s används redan." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "Addressen %s%s%s används redan." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "Den publika SSH nyckeln %s%s%s används redan." + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "CAPTCHA saknas." + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "Denna CAPTCHA har löpt ut. Var god försök igen." + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "Det angivna CAPTCHA-svaret är ogiltigt." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "Fel vid skapande av kontot %s%s%s" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "Kontot %s%s%s har skapats framgångsrikt." + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "En lösenordsåterställnings nyckel har skickats till din e-mail adress." + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "Klicka på Inloggning länken ovan för att använda ditt konto." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "Inga ändringar gjordes till kontot, %s%s%s." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "Kontot %s%s%s har blivit modifierat framgångsrikt." + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "Inloggnings formuläret är för närvarande avstängt för din IP adress, troligen på grund av spam attacker. Vi ber om ursäkt för besväret." + +#: lib/acctfuncs.inc.php +msgid "Account suspended" +msgstr "Konto avstängt" + +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "Du har inte behörighet att stänga av konton." + +#: lib/acctfuncs.inc.php +#, php-format +msgid "" +"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 %sPassword Reset%s page." +msgstr "Ditt lösenord har blivit återställt. Om du precis skapade ett nytt konto, var god och följ länken i konfirmations mailet för att sätta ett första lösenord. Annars, var god och skicka in en förfrågan om en återställningsnyckel på %sLösenords Återställning%s sidan" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "Fel användarnamn eller lösenord." + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "Ett fel inträffade vid genererandet av en användar session." + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "Ogiltig e-mail och återställningsnyckel kombination." + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "Ingen" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "Visa konto information för %s" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "Paketbas ID eller paketbasnamn saknas." + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "Du har inte rättigheter till att redigera denna kommentar." + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "Kommentaren existerar inte." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "Kommentaren kan inte vara tom." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "Kommentaren har blivit tillagd." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "Du måste vara inloggad innan du kan redigera paket information." + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "Kommentars ID fattas." + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "Inte mer än 5 kommentarer kan bli fästa." + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "Du har inte rättigheter till att fästa denna kommentar." + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "Du har inte rättigheter till att lossa denna kommentar." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "Kommentaren har blivit fäst." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "Kommentaren har blivit lossad." + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "Fel vid hämtning av paket detaljer." + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "Paket detaljer kunde inte hittas." + +#: 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 "Du har inte valt några paket att notifieras om." + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "Du måste vara inloggad före du kan flagga paket." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "Du valde inte några paket att flagga." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "De valda paketen har inte blivit flaggade, var snäll och skriv in en kommentar." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "De valda paketen har blivit flaggade utdaterade." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "Du måste vara inloggad före du kan avflagga paket." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "Du valde inte några paket att avflagga." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "De valda paketen har blivit avflaggade." + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "Du har inte tillgång till att radera paket." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to delete." +msgstr "Du valde inte några paket att radera." + +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Ett av paketen du valde finns inte." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been deleted." +msgstr "De valda paketen har blivit raderade." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can adopt packages." +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can disown packages." +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to adopt." +msgstr "Du valde into några paket att adoptera." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "Du valde inte några paket att göra härrelösa." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "De valda paketen har blivit adopterade." + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "De valda paketen har blivit härrelösa." + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "Du måste vara inloggad före du kan rösta för paket" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "Du måste vara inloggad före du kan ta bort din röst från paket." + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "Du valde inte något paket att rösta för." + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "Dina röster har blivit borttagna från de valda paketen." + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "Dina röster har blivit sparade för de valda paketen." + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "Kunde inte lägga till i notifierings listan." + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "Du har blivit tillagd i kommentarnotifierings listan för %s" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "Du har blivit borttagen från kommentarsnotifierings listan för %s" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "Du har inte rättigheter att återta denna kommentar." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "Kommentaren har blivit återtagen." + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "Du kan inte ta bort den här kommentaren" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "Kommentaren har blivit borttagen." + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "Kommentaren har bilvit redigerad." + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "Du kan inte redigera nyckelorden för den här paket basen." + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "Paket basens nyckelord har blivit updaterade." + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "Du har inte tillåtelse att ändra sam-ansvariga för denna paket bas." + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "Felaktigt användar namn: %s" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "Paket basens sam-ansvariga har blivit updaterade" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "Visa paket detaljer för" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "behöver %s" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "Du måste vara inloggad för att öppna paket förfrågningar." + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "Felaktigt namn: bara gemener är tillåtna" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "Kommentarsfältet får inte vara tomt." + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "Felaktig förfrågningstyp" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "Lade till förfrågningen framgångsrikt." + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "Felaktig anledning" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "Bara TUs och Developers kan stänga förfrågningar." + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "Förfrågning stängt framgångsrikt" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "Du kan använda det här formuläret för att permanent radera AUR kontot %s." + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "%sVARNING%s: Den här handlingen kan inte ogöras." + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "Konfirmera radering" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "Användarnamn" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "Konto typ" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "Användare" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "Developer" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "Trusted User & Developer" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "Email Adress" + +#: template/account_details.php +msgid "hidden" +msgstr "gömd" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "Riktigt namn" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "Hemsida" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "IRC nick" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "PGP nyckel fingerprint" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "Status" + +#: template/account_details.php +msgid "Inactive since" +msgstr "Inaktiv sen" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "Aktiv" + +#: template/account_details.php +msgid "Registration date:" +msgstr "Registrerings datum:" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "okänd" + +#: template/account_details.php +msgid "Last Login" +msgstr "Sista inloggning" + +#: template/account_details.php +msgid "Never" +msgstr "Aldrig" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "Visa den här användarens paket" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "Redigera den här användarens konto" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "Lista denna användarens kommentarer" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "Klicka %shär%s om du vill permanent radera detta konto." + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "Klicka %shär%s för användarinformation." + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "Klicka %shär%s för att lista kommentarerna från detta konto." + +#: template/account_edit_form.php +msgid "required" +msgstr "krävs" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "Ditt användarnamn är det namn du kommer att använda för att logga in. Det är synligt för allmänheten, även om ditt konto är inaktivt." + +#: template/account_edit_form.php template/search_accounts_form.php +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" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "Inaktiv" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "Var god och se till att du har skrivit in din epostadress korrect, annars kommer du bli utelåst." + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "Göm epostadress" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +msgstr "Backup e-postadress" + +#: 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 "Ange eventuellt en sekundär e-postadress som kan användas för att återställa ditt konto om du förlorar åtkomsten till din primära e-postadress." + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "Språk" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "Tidszon" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "Om du vill ändra lösenordet, ange ett nytt lösenord och bekräfta det nya lösenordet genom att ange det igen." + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "Skriv lösenord igen" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "Den följande informationen är bara nödvändig om du vill ladda upp paket till Arch User Repository." + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "SSH publik nyckel" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "Notifieringsinställningar" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "Notifiera om nya kommentarer" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "Notifiera om paketupdateringar" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "Notifiera om ägarskaps ändringar." + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "För att bekräfta profiländringarna, vänligen ange ditt nuvarande lösenord:" + +#: template/account_edit_form.php +msgid "Your current password" +msgstr "Ditt nuvarande lösenord" + +#: 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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "Svar" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "Updatera" + +#: template/account_edit_form.php +msgid "Create" +msgstr "Skapa" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "Återställ" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "Inga resultat matchade dina sök kriterier." + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "Redigera konto" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "Stäng av" + +#: template/account_search_results.php +msgid "Edit" +msgstr "Redigera" + +#: template/account_search_results.php +msgid "Less" +msgstr "Mindre" + +#: template/account_search_results.php +msgid "More" +msgstr "Mer" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "Inga mer resultat att visa." + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "Använd den här formen för att lägga till sam-ansvariga för %s%s%s (ett användarnamn per linje):" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "Användare" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "Spara" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "Anledning till flaggning som gammal: %s" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "%s%s%s flaggades %s%s%s som gammalt på %s%s%s av följande anledning:" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "%s%s%s är inte flaggad som gammal." + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "Återvänd till Detaljer" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "Upphovsrätt %s 2004-%d aurweb utvecklingsteamet" + +#: template/header.php +msgid " My Account" +msgstr " Mitt konto" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "Paket handlingar" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "Visa PKGBUILD" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "Visa ändringar" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "Ladda ned ögonblicksbild" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "Sök wiki" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "Flagga som gammalt (%s)" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "Flagga paketet utdaterat" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "Avflagga paket" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "Ta bort röst" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "Rösta för detta paket" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "Stäng av notifieringar" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "Email notifieringar" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "Hantera sam-ansvariga" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "%d väntande förfrågning" +msgstr[1] "%d väntande förfrågningar" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "Adoptera paket" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "Paket bas detaljer" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "Git klonings URL" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "skrivskyddad" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "klicka för att kopiera" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "Nyckelord" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "Första uppladdare" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "Underhållare" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "Sista paketerare" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "Röster" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "Populäritet" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "Först uppladdat" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "Sist updaterat" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "Redigera kommentar för: %s" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "Lägg till kommentar" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "%sMarkdown syntax%s stöds delvis." + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "Fästa Kommentarer" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "Senaste kommentarer" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "Kommentarer för" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "%s kommenterade %s" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "Anonym kommentar från %s" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "Kommenterade på paket %s på %s" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "raderad %s av %s" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "raderad %s" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "redigerad %s av %s" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "redigerad %s" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "Återta kommentaren" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "Radera kommentar" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "Fäst kommentar" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "Lossa kommentar" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "Paket detaljer" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "Paket bas" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "Beskrivning" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "Uppströms URL" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "Besök hemsidan för" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "Licenser" + +#: template/pkg_details.php +msgid "Groups" +msgstr "Grupper" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "Konflikter" + +#: template/pkg_details.php +msgid "Provides" +msgstr "Erbjuder" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "Ersätter" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "Beroenden" + +#: template/pkg_details.php +msgid "Required by" +msgstr "Krävs av" + +#: template/pkg_details.php +msgid "Sources" +msgstr "Källor" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "Använd det här formuläret för att stänga förfrågningen för paket basen %s%s%s." + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "Kommentarsfältet kan lämnas tomt, men det är rekommenderat att lämna en kommentar när man avböjer en förfrågning." + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "Anledning" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "Accepterad" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "Avböjd" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "Använd det här formuläret för att öppna en förfrågning mot paket bas %s%s%s, villket inkluderar de följande paketen:" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "Förfrågningstyp" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "Radering" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "Herrelös" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "Inga förfrågningar matchade dina sökkriterier." + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "%d paket förfrågning hittad." +msgstr[1] "%d paket förfrågningar hittade." + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "Sida %d av %d" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "Paket" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "Öppnad av" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "Datum" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "~%d dag kvar" +msgstr[1] "~%d dagar kvar" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "~%d timme kvar" +msgstr[1] "~%d timmar kvar" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "<1 timme kvar" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "Acceptera" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "Låst" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "Stäng" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "Stängd" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "Namn, Beskrivning" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "Bara namn" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "Exakt namn" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "Exakt paket bas" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "Sam-ansvarig" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "Ansvarig, Sam-ansvariga" + +#: template/pkg_search_form.php +msgid "All" +msgstr "Alla" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "Flaggade" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "Inte flaggade" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "Namn" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "Röstade" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "Senast modifierad" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "Stigande" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "Fallande" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "Skriv in sök kriterier" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "Sök efter" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "Utdaterat" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "Sortera efter" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "Sorterings ordning" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "Per sida" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "Gå" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "Herrelösa" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "Fel inträffade vid hämt av paket listan." + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "Inga paket matchade dina sök kriterier." + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "%d paket hittat" +msgstr[1] "%d paket hittade" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "Version" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "Ja" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "herrelös" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "Handlingar" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "Avflagga utdaterat" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "Adoptera paketen" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "Gör paket herrelösa" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "Radera paketen" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "Konfirmera" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "Alla typer" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "Sök" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "Statistik" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "Härrelösa paket" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "Paket tillagda de senaste 7 dagarna" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "Paket uppdaterade de senaste 7 dagarna" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "Paket uppdaterade the senaste året" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "Paket aldrig updaterade" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "Registrerade användare" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "Trusted Users" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "Nyliga uppdateringar" + +#: template/stats/updates_table.php +msgid "more" +msgstr "mer" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "Mina statistiker" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "Förslagsdetaljer" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "Den här omröstningen är fortfarande öppen" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "Uppladdad: %s av %s" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "Slut" + +#: template/tu_details.php +msgid "Result" +msgstr "Resultat" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "Nej" + +#: template/tu_details.php +msgid "Abstain" +msgstr "Avstå" + +#: template/tu_details.php +msgid "Total" +msgstr "Totalt" + +#: template/tu_details.php +msgid "Participation" +msgstr "Deltagande" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "Sista röstning per TU" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "Sista röstning" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "Inga resultat hittades." + +#: template/tu_list.php +msgid "Start" +msgstr "Start" + +#: template/tu_list.php +msgid "Back" +msgstr "Backåt" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "AUR Återställ Lösenord" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "AUR Kommentar för {pkgbase}" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "{user} [1] lade till följande kommentar till {pkgbase} [2]:" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "AUR paket uppdatering: {pkgbase}" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "AUR paket borttaget: {pkgbase}" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Ogiltig kontotyp har angetts." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "Du har inte behörighet att ändra kontotyper." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +msgstr "Du har inte behörighet att ändra den här användarens kontotyp till %s." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "" + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/tr.po b/po/tr.po index 04161700..83b1e4df 100644 --- a/po/tr.po +++ b/po/tr.po @@ -5,7 +5,8 @@ # Translators: # tarakbumba , 2011,2013-2015 # tarakbumba , 2012,2014 -# Demiray Muhterem , 2015 +# Demiray Muhterem , 2015,2020-2021 +# Koray Biçer , 2020 # Lukas Fleischer , 2011 # Samed Beyribey , 2012 # Samed Beyribey , 2012 @@ -16,8 +17,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -39,7 +40,7 @@ msgstr "Not" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "Git clone linkleri bir tarayıcıda açılamazlar." +msgstr "Git clone linkleri web tarayıcıda açılamazlar." #: html/404.php #, php-format @@ -82,7 +83,7 @@ msgstr "Bu hesap üzerinde değişiklik yapma izniniz yok." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Yanlış şifre." #: html/account.php msgid "Use this form to search existing accounts." @@ -172,7 +173,7 @@ msgstr "Yorumu düzenle" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Kontrol Paneli" #: html/home.php template/header.php msgid "Home" @@ -180,11 +181,11 @@ msgstr "Anasayfa" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "İşaretli Paketlerim" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "İsteklerim" #: html/home.php msgid "My Packages" @@ -192,15 +193,15 @@ msgstr "Paketlerim" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Bakımını yaptığım paketleri ara" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Ortak Bakılan Paketler" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Birlikte baktığım paketleri ara" #: html/home.php #, php-format @@ -330,7 +331,7 @@ msgid "" "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 "" +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." #: html/home.php msgid "Package Search" @@ -338,7 +339,7 @@ msgstr "Paket Ara" #: html/index.php msgid "Adopt" -msgstr "Sorumluluğunu Al" +msgstr "Sahiplen" #: html/index.php msgid "Vote" @@ -379,7 +380,7 @@ msgstr "Giriş bilgilerinizi doldurun" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Kullanıcı adı veya birincil e-posta adresi" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -443,7 +444,7 @@ msgstr "Parolanız başarıyla sıfırlandı." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Kullanıcı adınızı veya birincil e-posta adresinizi doğrulayın:" #: html/passreset.php msgid "Enter your new password:" @@ -462,15 +463,15 @@ msgstr "Devam" 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 "Kaydettiğiniz kullanıcı adını ve birincil e-posta adresini unuttuysanız, lütfen %s genel %s posta listesine bir mesaj gönderin." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Kullanıcı adınızı veya birincil e-posta adresinizi girin:" #: html/pkgbase.php msgid "Package Bases" -msgstr "" +msgstr "Paket Tabanları" #: html/pkgbase.php msgid "" @@ -478,6 +479,12 @@ msgid "" "checkbox." msgstr "Seçilen paketler için sahiplik bırakılamadı. Onaylama kutucuğunu işaretleyin." +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "Seçilen paketler kabul edilmedi, onay kontrol kutusunu işaretleyin." + #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." msgstr "Oyların ve yorumların ilişkilendirilebileceği herhangi bir paket bulunamadı." @@ -542,7 +549,7 @@ msgstr "Şu paketleri içeren %s%s%s paket temelinin sahipliğini bırakmak içi msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "Onay kutusunu işaretleyerek, artık bir paket yardımcısı olmak istemediğinizi onaylarsınız." #: html/pkgdisown.php #, php-format @@ -576,19 +583,27 @@ msgstr "Yorumu İşaretle" msgid "Flag Package Out-Of-Date" msgstr "Paketi güncel değil olarak işaretle" +#: 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 "Bu bir VCS paketi gibi görünüyor. AUR'daki paket sürümü en son taahhütle eşleşmiyorsa, lütfen güncelliğini %sdeğil%s olarak işaretleyin. Bu paketin işaretlenmesi, yalnızca, son yukarı akış değişiklikleri nedeniyle PKGBUILD içindeki kaynaklar taşınmışsa veya değişiklikler gerekliyse yapılmalıdır." + #: html/pkgflag.php #, php-format msgid "" "Use this form to flag the package base %s%s%s and the following packages " "out-of-date: " -msgstr "" +msgstr "%s%s%s paket tabanını ve aşağıdaki paketleri güncel olmayan olarak işaretlemek için bu formu kullanın:" #: html/pkgflag.php #, php-format msgid "" "Please do %snot%s use this form to report bugs. Use the package comments " "instead." -msgstr "" +msgstr "Lütfen %s değil, %s hataları bildirmek için bu formu kullanın. Bunun yerine paket yorumlarını kullanın." #: html/pkgflag.php msgid "" @@ -652,7 +667,7 @@ msgstr "Sadece geliştiriciler ve güvenilir kullanıcılar paket birleştirebil #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" -msgstr "" +msgstr "İstek Oluştur" #: html/pkgreq.php template/pkgreq_close_form.php msgid "Close Request" @@ -688,21 +703,21 @@ msgstr "Yeni bir hesap oluşturmak için bu formu kullanın." #: html/tos.php msgid "Terms of Service" -msgstr "" +msgstr "Kullanım Şartları" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "" +msgstr "Aşağıdaki belgeler güncellendi. Lütfen bunları dikkatle inceleyin:" #: html/tos.php #, php-format msgid "revision %d" -msgstr "" +msgstr "düzeltme %d" #: html/tos.php msgid "I accept the terms and conditions above." -msgstr "" +msgstr "Yukarıdaki şartlar ve koşulları kabul ediyorum." #: html/tu.php template/account_details.php template/header.php msgid "Trusted User" @@ -773,7 +788,7 @@ msgstr "Sadece bir nokta, alt çizgi veya tire barındırabilir." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Lütfen yeni şifrenizi onaylayın." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -781,11 +796,11 @@ msgstr "E-posta adresi geçerli değil." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Yedekleme e-posta adresi geçersiz." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." -msgstr "" +msgstr "Ana sayfa geçersiz, lütfen tam HTTP(li) URL belirtin." #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." @@ -805,7 +820,7 @@ msgstr "Dil henüz desteklenmiyor." #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." -msgstr "" +msgstr "Saat dilimi şu anda desteklenmemektedir." #: lib/acctfuncs.inc.php #, php-format @@ -824,15 +839,15 @@ msgstr "SSH kamu anahtarı, %s%s%s, zaten kullanımda." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "CAPTCHA eksik." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "Bu CAPTCHA'nın süresi doldu. Lütfen tekrar deneyin." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "Girilen CAPTCHA yanıtı geçersiz." #: lib/acctfuncs.inc.php #, php-format @@ -872,6 +887,10 @@ msgstr "Muhtemelen sürekli spam saldırıları olması nedeniyle IP adresinizde msgid "Account suspended" msgstr "Hesap donduruldu" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "Hesapları askıya alma izniniz yok." + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -939,7 +958,7 @@ msgstr "Bu yorumu iğnelemek için yetkiniz yok." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to unpin this comment." -msgstr "" +msgstr "Bu yorumun sabitlemesini kaldırma izniniz yok." #: lib/pkgbasefuncs.inc.php msgid "Comment has been pinned." @@ -947,7 +966,7 @@ msgstr "Yorum iğnelendi." #: lib/pkgbasefuncs.inc.php msgid "Comment has been unpinned." -msgstr "" +msgstr "Yorum sabitlemesi kaldırıldı" #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Error retrieving package details." @@ -957,6 +976,30 @@ msgstr "Paket bilgileri alınırken hata oluştu." msgid "Package details could not be found." msgstr "Paket ayrıntıları bulunamadı." +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "Kötü Yönlendirici başlık." + +#: aurweb/routers/packages.py +msgid "You did not select any packages to be notified about." +msgstr "Bildirilecek herhangi bir paket seçmediniz." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "Seçilen paketlerin bildirimleri etkinleştirildi." + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "Bildirim kaldırma için herhangi bir paket seçmediniz." + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "Seçtiğiniz bir pakette bildirimler etkin değil." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." +msgstr "Seçilen paketlerin bildirimleri kaldırıldı." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." msgstr "Paketleri işaretleyebilmek için giriş yapmalısınız." @@ -993,6 +1036,10 @@ msgstr "Paket silmek için gerekli izne sahip değilsiniz." msgid "You did not select any packages to delete." msgstr "Silinecek paketleri seçmediniz." +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Seçtiğiniz paketlerden biri mevcut değil." + #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." msgstr "Seçilen paketler silindi." @@ -1001,10 +1048,18 @@ msgstr "Seçilen paketler silindi." msgid "You must be logged in before you can adopt packages." msgstr "Paketleri sahiplenmek için giriş yapmalısınız." +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "Seçtiğiniz paketlerden birini benimsemenize izin verilmiyor." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." msgstr "Paketlerin sahipliğini bırakmak için giriş yapmalısınız." +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "Seçtiğiniz paketlerden birini reddetme izniniz yok." + #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." msgstr "Sahiplenilecek paketleri seçmediniz." @@ -1245,7 +1300,7 @@ msgstr "Bu kullanıcının hesabını düzenleyin" #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Bu kullanıcının yorumlarını listele" #: template/account_edit_form.php #, php-format @@ -1260,7 +1315,7 @@ msgstr "Kullanıcı detayları için %sburaya%s tıklayın." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Bu hesap tarafından yapılan yorumları listelemek için %s buraya %s tıklayın." #: template/account_edit_form.php msgid "required" @@ -1270,7 +1325,7 @@ msgstr "gerekli" msgid "" "Your user name is the name you will use to login. It is visible to the " "general public, even if your account is inactive." -msgstr "" +msgstr "Kullanıcı adınız, oturum açmak için kullanacağınız addır. Hesabınız etkin olmasa bile, herkes tarafından görülebilir." #: template/account_edit_form.php template/search_accounts_form.php msgid "Normal user" @@ -1303,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 "E-posta adresinizi gizlemezseniz, tüm kayıtlı AUR kullanıcıları tarafından görülebilir. E-posta adresinizi gizlerseniz, adres yalnızca Arch Linux personeli tarafından görülebilir." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Yedekleme E-posta Adresi" #: 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 "İsteğe bağlı olarak, birincil e-posta adresinize erişiminizi kaybetmeniz durumunda hesabınızı geri yüklemek için kullanılabilecek ikincil bir e-posta adresi sağlayın." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Parola sıfırlama bağlantıları her zaman hem birincil adresinize hem de yedek e-posta adresinize gönderilir." #: 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 "Yedek e-posta adresiniz her zaman %s ayarından bağımsız olarak yalnızca Arch Linux personeli tarafından görülebilir." #: template/account_edit_form.php msgid "Language" @@ -1334,13 +1389,13 @@ msgstr "Dil" #: template/account_edit_form.php msgid "Timezone" -msgstr "" +msgstr "Saat dilimi" #: template/account_edit_form.php msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Şifreyi değiştirmek istiyorsanız, yeni bir şifre girin ve yeni şifreyi tekrar girerek onaylayın." #: template/account_edit_form.php msgid "Re-type password" @@ -1374,21 +1429,21 @@ msgstr "Sahiplik değişikliklerini bildir." #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Profil değişikliklerini onaylamak için lütfen mevcut şifrenizi girin:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Mevcut şifreniz" #: 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 otomatik hesap oluşturmaya karşı korumak için sizden aşağıdaki komutun çıktısını sağlamanızı rica ediyoruz:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Cevap" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1453,7 +1508,7 @@ msgstr "Güncel olmayan olarak işaretli yorum: %s" #: template/flag_comment.php #, php-format msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" -msgstr "" +msgstr "%s%s%s, aşağıdaki nedenlerden dolayı %s%s%s'i %s%s%s olarak işaretledi:" #: template/flag_comment.php #, php-format @@ -1467,7 +1522,7 @@ msgstr "Detaylara Geri Dön." #: template/footer.php #, php-format msgid "Copyright %s 2004-%d aurweb Development Team." -msgstr "" +msgstr "Telif Hakkı %s 2004-%d aurweb Gelişim Takımı." #: template/header.php msgid " My Account" @@ -1551,7 +1606,7 @@ msgstr "salt okunur" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "kopyalamak için tıkla" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1603,12 +1658,12 @@ msgstr "Yorum Ekle" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "AUR paketindeki işlemleri referans alan Git işlem tanımlayıcıları ve URL adresleri otomatik olarak bağlantılara dönüştürülür." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "%s Markdown sözdizimi %s kısmen desteklenmektedir." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1620,7 +1675,7 @@ msgstr "Son Yorumlar" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Yorum" #: template/pkg_comments.php #, php-format @@ -1635,7 +1690,7 @@ msgstr "%s'e isimsiz yorum" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "%s paketinde %s yorumladı" #: template/pkg_comments.php #, php-format @@ -1671,7 +1726,7 @@ msgstr "Yorumu iğnele" #: template/pkg_comments.php msgid "Unpin comment" -msgstr "" +msgstr "Yorum sabitlemesini kaldır" #: template/pkg_details.php msgid "Package Details" @@ -1794,11 +1849,11 @@ msgid "" "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 "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." #: template/pkgreq_results.php msgid "No requests matched your search criteria." -msgstr "" +msgstr "Arama kriterlerinize uyan istek yok." #: template/pkgreq_results.php #, php-format @@ -1856,7 +1911,7 @@ msgstr "Kapat" #: template/pkgreq_results.php msgid "Pending" -msgstr "" +msgstr "Bekliyor" #: template/pkgreq_results.php msgid "Closed" @@ -1880,11 +1935,11 @@ msgstr "Tam Paket Temeli" #: template/pkg_search_form.php msgid "Co-maintainer" -msgstr "" +msgstr "Yardımcı Bakıcı" #: template/pkg_search_form.php msgid "Maintainer, Co-maintainer" -msgstr "" +msgstr "Bakıcı, Yardımcı Bakıcı" #: template/pkg_search_form.php msgid "All" @@ -2056,7 +2111,7 @@ msgstr "Son Güncellemeler" #: template/stats/updates_table.php msgid "more" -msgstr "" +msgstr "daha fazla" #: template/stats/user_table.php msgid "My Statistics" @@ -2121,7 +2176,7 @@ msgstr "Geri" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "" +msgstr "AUR Şifre Sıfırlama" #: scripts/notify.py #, python-brace-format @@ -2129,90 +2184,90 @@ 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 "E-posta adresinizle ilişkili {user} hesabı için bir şifre sıfırlama isteği gönderildi. Şifrenizi sıfırlamak isterseniz, aşağıdaki bağlantıyı [1] takip edin, aksi takdirde bu mesajı dikkate almayın; hiçbir şey olmaz." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "Arch Kullanıcı Deposuna Hoş Geldiniz" #: 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 "Arch Kullanıcı Deposuna Hoş Geldiniz! Yeni hesabınız için bir başlangıç şifresi belirlemek için lütfen aşağıdaki bağlantıyı [1] tıklayın. Bağlantı çalışmazsa, kopyalayıp tarayıcınıza yapıştırmayı deneyin." #: scripts/notify.py #, python-brace-format msgid "AUR Comment for {pkgbase}" -msgstr "" +msgstr "{pkgbase} için yorum yapın" #: scripts/notify.py #, python-brace-format msgid "{user} [1] added the following comment to {pkgbase} [2]:" -msgstr "" +msgstr "{user} [1], {pkgbase} [2] ürününe şu yorumu ekledi:" #: scripts/notify.py #, python-brace-format msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Artık bu paket hakkında bildirim almak istemiyorsanız, lütfen paket sayfasına [2] gidin ve \"{label}\" seçeneğini seçin." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "Diğer paket güncellemeleri: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} [1], {pkgbase} [2] için yeni bir işlemde bulundu." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} için AUR Güncel Değil Bildirimi" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "{pkgbase} [1] paketiniz {user} [2] tarafından eski olarak işaretlendi:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} için AUR Sahiplik Bildirimi" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "{pkgbase} [1] paketi {user} [2] tarafından kabul edildi." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "{pkgbase} [1] paketi {user} [2] tarafından reddedildi." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "{pkgbase} için AUR Ortak Bakıcı bildirimi" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "{pkgbase} [1] ortak bakıcı listesine eklendi." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "{pkgbase} [1] adlı ortak bakıcı listesinden çıkarıldınız." #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "AUR Paketi silindi: {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2221,7 +2276,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] ile {new} [3] arasını birleştirdi.\n\n-- \nEğer yeni paket hakkında bildirim almak istemiyorsanız, lütfen [3] 'e gidin ve \"{label}\" düğmesini tıklayın." #: scripts/notify.py #, python-brace-format @@ -2229,16 +2284,61 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] {pkgbase} [2] paketini sildi.\n\nArtık bu paket hakkında bildirim almayacaksınız." #: scripts/notify.py #, python-brace-format msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgstr "TU Oylama Hatırlatıcısı: Teklif {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 "Lütfen {id} [1] teklifine oy vermeyi unutmayın. Oylama süresi 48 saatten az sürer." + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Geçersiz hesap türü sağlandı." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "Hesap türlerini değiştirme izniniz yok." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +msgstr "Bu kullanıcının hesap türünü %s olarak değiştirme izniniz yok." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "%s için kabul edilecek sahipsize gereksinim yok." + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/uk.po b/po/uk.po index 7dd65d90..a4410185 100644 --- a/po/uk.po +++ b/po/uk.po @@ -14,8 +14,8 @@ msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -476,6 +476,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 "Не вдалося знайти пакунок для об’єднання голосів та коментарів." @@ -574,6 +580,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 "" @@ -870,6 +884,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 "" @@ -955,6 +973,30 @@ msgstr "Помилка пошуку інформації про пакунок." msgid "Package details could not be found." msgstr "Інформації про пакунок не знайдено." +#: 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 msgid "You must be logged in before you can flag packages." msgstr "Для встановлення мітки слід увійти." @@ -991,6 +1033,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 "Вибрані пакунки вилучено." @@ -999,10 +1045,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 "Ви не вибрали жодного пакунку для переймання." @@ -2250,3 +2304,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "Будь ласка, не забудьте подати свій голос на пропозицію {id} [1]. Голосування закінчиться за менш ніж 48 годин." + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/vi.po b/po/vi.po new file mode 100644 index 00000000..3ea5bad3 --- /dev/null +++ b/po/vi.po @@ -0,0 +1,2330 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: vi\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/zh.po b/po/zh.po new file mode 100644 index 00000000..04fe06f3 --- /dev/null +++ b/po/zh.po @@ -0,0 +1,2330 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the AURWEB package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: aurweb\n" +"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: html/404.php +msgid "Page Not Found" +msgstr "" + +#: html/404.php +msgid "Sorry, the page you've requested does not exist." +msgstr "" + +#: html/404.php template/pkgreq_close_form.php +msgid "Note" +msgstr "" + +#: html/404.php +msgid "Git clone URLs are not meant to be opened in a browser." +msgstr "" + +#: html/404.php +#, php-format +msgid "To clone the Git repository of %s, run %s." +msgstr "" + +#: html/404.php +#, php-format +msgid "Click %shere%s to return to the %s details page." +msgstr "" + +#: html/503.php +msgid "Service Unavailable" +msgstr "" + +#: html/503.php +msgid "" +"Don't panic! This site is down due to maintenance. We will be back soon." +msgstr "" + +#: html/account.php +msgid "Account" +msgstr "" + +#: html/account.php template/header.php +msgid "Accounts" +msgstr "" + +#: html/account.php html/addvote.php +msgid "You are not allowed to access this area." +msgstr "" + +#: html/account.php +msgid "Could not retrieve information for the specified user." +msgstr "" + +#: html/account.php +msgid "You do not have permission to edit this account." +msgstr "" + +#: html/account.php lib/acctfuncs.inc.php +msgid "Invalid password." +msgstr "" + +#: html/account.php +msgid "Use this form to search existing accounts." +msgstr "" + +#: html/account.php +msgid "You must log in to view user information." +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Add Proposal" +msgstr "" + +#: html/addvote.php +msgid "Invalid token for user action." +msgstr "" + +#: html/addvote.php +msgid "Username does not exist." +msgstr "" + +#: html/addvote.php +#, php-format +msgid "%s already has proposal running for them." +msgstr "" + +#: html/addvote.php +msgid "Invalid type." +msgstr "" + +#: html/addvote.php +msgid "Proposal cannot be empty." +msgstr "" + +#: html/addvote.php +msgid "New proposal submitted." +msgstr "" + +#: html/addvote.php +msgid "Submit a proposal to vote on." +msgstr "" + +#: html/addvote.php +msgid "Applicant/TU" +msgstr "" + +#: html/addvote.php +msgid "(empty if not applicable)" +msgstr "" + +#: html/addvote.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Type" +msgstr "" + +#: html/addvote.php +msgid "Addition of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU" +msgstr "" + +#: html/addvote.php +msgid "Removal of a TU (undeclared inactivity)" +msgstr "" + +#: html/addvote.php +msgid "Amendment of Bylaws" +msgstr "" + +#: html/addvote.php template/tu_list.php +msgid "Proposal" +msgstr "" + +#: html/addvote.php +msgid "Submit" +msgstr "" + +#: html/comaintainers.php template/comaintainers_form.php +msgid "Manage Co-maintainers" +msgstr "" + +#: html/commentedit.php template/pkg_comments.php +msgid "Edit comment" +msgstr "" + +#: html/home.php template/header.php +msgid "Dashboard" +msgstr "" + +#: html/home.php template/header.php +msgid "Home" +msgstr "" + +#: html/home.php +msgid "My Flagged Packages" +msgstr "" + +#: html/home.php +msgid "My Requests" +msgstr "" + +#: html/home.php +msgid "My Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I maintain" +msgstr "" + +#: html/home.php +msgid "Co-Maintained Packages" +msgstr "" + +#: html/home.php +msgid "Search for packages I co-maintain" +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 "" + +#: html/home.php +#, php-format +msgid "" +"Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " +"otherwise they will be deleted!" +msgstr "" + +#: html/home.php +msgid "Remember to vote for your favourite packages!" +msgstr "" + +#: html/home.php +msgid "Some packages may be provided as binaries in [community]." +msgstr "" + +#: html/home.php +msgid "DISCLAIMER" +msgstr "" + +#: html/home.php template/footer.php +msgid "" +"AUR packages are user produced content. Any use of the provided files is at " +"your own risk." +msgstr "" + +#: html/home.php +msgid "Learn more..." +msgstr "" + +#: html/home.php +msgid "Support" +msgstr "" + +#: html/home.php +msgid "Package Requests" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"There are three types of requests that can be filed in the %sPackage " +"Actions%s box on the package details page:" +msgstr "" + +#: html/home.php +msgid "Orphan Request" +msgstr "" + +#: 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 "" + +#: html/home.php +msgid "Deletion Request" +msgstr "" + +#: 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." +msgstr "" + +#: html/home.php +msgid "Merge Request" +msgstr "" + +#: html/home.php +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 "" + +#: 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 "" + +#: html/home.php +msgid "Submitting Packages" +msgstr "" + +#: html/home.php +#, php-format +msgid "" +"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." +msgstr "" + +#: html/home.php +msgid "The following SSH fingerprints are used for the AUR:" +msgstr "" + +#: html/home.php +msgid "Discussion" +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 "" + +#: html/home.php +msgid "Bug Reporting" +msgstr "" + +#: html/home.php +#, php-format +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 "" + +#: html/home.php +msgid "Package Search" +msgstr "" + +#: html/index.php +msgid "Adopt" +msgstr "" + +#: html/index.php +msgid "Vote" +msgstr "" + +#: html/index.php +msgid "UnVote" +msgstr "" + +#: html/index.php template/pkg_search_form.php template/pkg_search_results.php +msgid "Notify" +msgstr "" + +#: html/index.php template/pkg_search_results.php +msgid "UnNotify" +msgstr "" + +#: html/index.php +msgid "UnFlag" +msgstr "" + +#: html/login.php template/header.php +msgid "Login" +msgstr "" + +#: html/login.php html/tos.php +#, php-format +msgid "Logged-in as: %s" +msgstr "" + +#: html/login.php template/header.php +msgid "Logout" +msgstr "" + +#: html/login.php +msgid "Enter login credentials" +msgstr "" + +#: html/login.php +msgid "User name or primary email address" +msgstr "" + +#: html/login.php template/account_delete.php template/account_edit_form.php +msgid "Password" +msgstr "" + +#: html/login.php +msgid "Remember me" +msgstr "" + +#: html/login.php +msgid "Forgot Password" +msgstr "" + +#: html/login.php +#, php-format +msgid "" +"HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." +msgstr "" + +#: html/packages.php template/pkg_search_form.php +msgid "Search Criteria" +msgstr "" + +#: html/packages.php template/header.php template/pkgbase_details.php +#: template/stats/general_stats_table.php template/stats/user_table.php +msgid "Packages" +msgstr "" + +#: html/packages.php +msgid "Error trying to retrieve package details." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Missing a required field." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +msgid "Password fields do not match." +msgstr "" + +#: html/passreset.php lib/acctfuncs.inc.php +#, php-format +msgid "Your password must be at least %s characters." +msgstr "" + +#: html/passreset.php +msgid "Invalid e-mail." +msgstr "" + +#: html/passreset.php +msgid "Password Reset" +msgstr "" + +#: html/passreset.php +msgid "Check your e-mail for the confirmation link." +msgstr "" + +#: html/passreset.php +msgid "Your password has been reset successfully." +msgstr "" + +#: html/passreset.php +msgid "Confirm your user name or primary e-mail address:" +msgstr "" + +#: html/passreset.php +msgid "Enter your new password:" +msgstr "" + +#: html/passreset.php +msgid "Confirm your new password:" +msgstr "" + +#: html/passreset.php html/tos.php +msgid "Continue" +msgstr "" + +#: html/passreset.php +#, php-format +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 "" + +#: html/passreset.php +msgid "Enter your user name or your primary e-mail address:" +msgstr "" + +#: html/pkgbase.php +msgid "Package Bases" +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been disowned, check the confirmation " +"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 "" + +#: html/pkgbase.php lib/pkgreqfuncs.inc.php +msgid "Cannot merge a package base with itself." +msgstr "" + +#: html/pkgbase.php +msgid "" +"The selected packages have not been deleted, check the confirmation " +"checkbox." +msgstr "" + +#: html/pkgdel.php +msgid "Package Deletion" +msgstr "" + +#: html/pkgdel.php template/pkgbase_actions.php +msgid "Delete Package" +msgstr "" + +#: html/pkgdel.php +#, php-format +msgid "" +"Use this form to delete the package base %s%s%s and the following packages " +"from the AUR: " +msgstr "" + +#: html/pkgdel.php +msgid "Deletion of a package is permanent. " +msgstr "" + +#: html/pkgdel.php html/pkgmerge.php +msgid "Select the checkbox to confirm action." +msgstr "" + +#: html/pkgdel.php +msgid "Confirm package deletion" +msgstr "" + +#: html/pkgdel.php template/account_delete.php +msgid "Delete" +msgstr "" + +#: html/pkgdel.php +msgid "Only Trusted Users and Developers can delete packages." +msgstr "" + +#: html/pkgdisown.php template/pkgbase_actions.php +msgid "Disown Package" +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"Use this form to disown the package base %s%s%s which includes the following" +" packages: " +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to no longer be a " +"package co-maintainer." +msgstr "" + +#: html/pkgdisown.php +#, php-format +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package " +"and transfer ownership to %s%s%s." +msgstr "" + +#: html/pkgdisown.php +msgid "" +"By selecting the checkbox, you confirm that you want to disown the package." +msgstr "" + +#: html/pkgdisown.php +msgid "Confirm to disown the package" +msgstr "" + +#: html/pkgdisown.php +msgid "Disown" +msgstr "" + +#: html/pkgdisown.php +msgid "Only Trusted Users and Developers can disown packages." +msgstr "" + +#: html/pkgflagcomment.php +msgid "Flag Comment" +msgstr "" + +#: html/pkgflag.php +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 "" +"Use this form to flag the package base %s%s%s and the following packages " +"out-of-date: " +msgstr "" + +#: html/pkgflag.php +#, php-format +msgid "" +"Please do %snot%s use this form to report bugs. Use the package comments " +"instead." +msgstr "" + +#: html/pkgflag.php +msgid "" +"Enter details on why the package is out-of-date below, preferably including " +"links to the release announcement or the new release tarball." +msgstr "" + +#: html/pkgflag.php template/pkgreq_close_form.php template/pkgreq_form.php +#: template/pkgreq_results.php +msgid "Comments" +msgstr "" + +#: html/pkgflag.php +msgid "Flag" +msgstr "" + +#: html/pkgflag.php +msgid "Only registered users can flag packages out-of-date." +msgstr "" + +#: html/pkgmerge.php +msgid "Package Merging" +msgstr "" + +#: html/pkgmerge.php template/pkgbase_actions.php +msgid "Merge Package" +msgstr "" + +#: html/pkgmerge.php +#, php-format +msgid "Use this form to merge the package base %s%s%s into another package. " +msgstr "" + +#: html/pkgmerge.php +msgid "The following packages will be deleted: " +msgstr "" + +#: html/pkgmerge.php +msgid "Once the package has been merged it cannot be reversed. " +msgstr "" + +#: html/pkgmerge.php +msgid "Enter the package name you wish to merge the package into. " +msgstr "" + +#: html/pkgmerge.php +msgid "Merge into:" +msgstr "" + +#: html/pkgmerge.php +msgid "Confirm package merge" +msgstr "" + +#: html/pkgmerge.php template/pkgreq_form.php +msgid "Merge" +msgstr "" + +#: html/pkgmerge.php +msgid "Only Trusted Users and Developers can merge packages." +msgstr "" + +#: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php +msgid "Submit Request" +msgstr "" + +#: html/pkgreq.php template/pkgreq_close_form.php +msgid "Close Request" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "First" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Previous" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php template/tu_list.php +msgid "Next" +msgstr "" + +#: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php +msgid "Last" +msgstr "" + +#: html/pkgreq.php template/header.php +msgid "Requests" +msgstr "" + +#: html/register.php template/header.php +msgid "Register" +msgstr "" + +#: html/register.php +msgid "Use this form to create an account." +msgstr "" + +#: html/tos.php +msgid "Terms of Service" +msgstr "" + +#: html/tos.php +msgid "" +"The following documents have been updated. Please review them carefully:" +msgstr "" + +#: html/tos.php +#, php-format +msgid "revision %d" +msgstr "" + +#: 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 "" + +#: html/tu.php +msgid "Could not retrieve proposal details." +msgstr "" + +#: html/tu.php +msgid "Voting is closed for this proposal." +msgstr "" + +#: html/tu.php +msgid "Only Trusted Users are allowed to vote." +msgstr "" + +#: html/tu.php +msgid "You cannot vote in an proposal about you." +msgstr "" + +#: html/tu.php +msgid "You've already voted for this proposal." +msgstr "" + +#: html/tu.php +msgid "Vote ID not valid." +msgstr "" + +#: html/tu.php template/tu_list.php +msgid "Current Votes" +msgstr "" + +#: html/tu.php +msgid "Past Votes" +msgstr "" + +#: html/voters.php template/tu_details.php +msgid "Voters" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"Account registration has been disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Missing User ID" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The username is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "It must be between %s and %s characters long" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Start and end with a letter or number" +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Can contain only one period, underscore or hyphen." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Please confirm your new password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The backup email address is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The home page is invalid, please specify the full HTTP(s) URL." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The PGP key fingerprint is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The SSH public key is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Cannot increase account permissions." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Language is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Timezone is not currently supported." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The username, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The address, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The SSH public key, %s%s%s, is already in use." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The CAPTCHA is missing." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "This CAPTCHA has expired. Please try again." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "The entered CAPTCHA answer is invalid." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "Error trying to create account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully created." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "A password reset key has been sent to your e-mail address." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Click on the Login link above to use your account." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "No changes were made to the account, %s%s%s." +msgstr "" + +#: lib/acctfuncs.inc.php +#, php-format +msgid "The account, %s%s%s, has been successfully modified." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "" +"The login form is currently disabled for your IP address, probably due to " +"sustained spam attacks. Sorry for the inconvenience." +msgstr "" + +#: lib/acctfuncs.inc.php +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 "" +"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 %sPassword Reset%s page." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Bad username or password." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "An error occurred trying to generate a user session." +msgstr "" + +#: lib/acctfuncs.inc.php +msgid "Invalid e-mail and reset key combination." +msgstr "" + +#: lib/aur.inc.php template/pkg_details.php +msgid "None" +msgstr "" + +#: lib/aur.inc.php template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "View account information for %s" +msgstr "" + +#: lib/aurjson.class.php +msgid "Package base ID or package base name missing." +msgstr "" + +#: lib/aurjson.class.php lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit this comment." +msgstr "" + +#: lib/aurjson.class.php +msgid "Comment does not exist." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment cannot be empty." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been added." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can edit package information." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Missing comment ID." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "No more than 5 comments can be pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to pin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to unpin this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been pinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been unpinned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Error retrieving package details." +msgstr "" + +#: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php +msgid "Package details could not be found." +msgstr "" + +#: 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 +msgid "You must be logged in before you can flag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to flag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have not been flagged, please enter a comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been flagged out-of-date." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can unflag packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to unflag." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been unflagged." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You do not have permission to delete packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +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 "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to disown." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been adopted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The selected packages have been disowned." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You must be logged in before you can un-vote for packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You did not select any packages to vote for." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been removed from the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Your votes have been cast for the selected packages." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Couldn't add to notification list." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been added to the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "You have been removed from the comment notification list for %s." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to undelete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been undeleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to delete this comment." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been deleted." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "Comment has been edited." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to edit the keywords of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base keywords have been updated." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "You are not allowed to manage co-maintainers of this package base." +msgstr "" + +#: lib/pkgbasefuncs.inc.php +#, php-format +msgid "Invalid user name: %s" +msgstr "" + +#: lib/pkgbasefuncs.inc.php +msgid "The package base co-maintainers have been updated." +msgstr "" + +#: lib/pkgfuncs.inc.php template/pkgbase_details.php +msgid "View packages details for" +msgstr "" + +#: lib/pkgfuncs.inc.php +#, php-format +msgid "requires %s" +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "You must be logged in to file package requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid name: only lowercase letters are allowed." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "The comment field must not be empty." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid request type." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Added request successfully." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Invalid reason." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Only TUs and developers can close requests." +msgstr "" + +#: lib/pkgreqfuncs.inc.php +msgid "Request closed successfully." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "You can use this form to permanently delete the AUR account %s." +msgstr "" + +#: template/account_delete.php +#, php-format +msgid "%sWARNING%s: This action cannot be undone." +msgstr "" + +#: template/account_delete.php +msgid "Confirm deletion" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Username" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Account Type" +msgstr "" + +#: template/account_details.php template/tu_details.php +#: template/tu_last_votes_list.php template/tu_list.php +msgid "User" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Trusted User & Developer" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/search_accounts_form.php +msgid "Email Address" +msgstr "" + +#: template/account_details.php +msgid "hidden" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "Real Name" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +msgid "Homepage" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php template/search_accounts_form.php +msgid "IRC Nick" +msgstr "" + +#: template/account_details.php template/account_edit_form.php +#: template/account_search_results.php +msgid "PGP Key Fingerprint" +msgstr "" + +#: template/account_details.php template/account_search_results.php +#: template/pkgreq_results.php +msgid "Status" +msgstr "" + +#: template/account_details.php +msgid "Inactive since" +msgstr "" + +#: template/account_details.php template/account_search_results.php +msgid "Active" +msgstr "" + +#: template/account_details.php +msgid "Registration date:" +msgstr "" + +#: template/account_details.php template/pkgbase_details.php +#: template/pkg_details.php template/pkgreq_results.php +#: template/tu_details.php +msgid "unknown" +msgstr "" + +#: template/account_details.php +msgid "Last Login" +msgstr "" + +#: template/account_details.php +msgid "Never" +msgstr "" + +#: template/account_details.php +msgid "View this user's packages" +msgstr "" + +#: template/account_details.php +msgid "Edit this user's account" +msgstr "" + +#: template/account_details.php +msgid "List this user's comments" +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s if you want to permanently delete this account." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s for user details." +msgstr "" + +#: template/account_edit_form.php +#, php-format +msgid "Click %shere%s to list the comments made by this account." +msgstr "" + +#: template/account_edit_form.php +msgid "required" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Your user name is the name you will use to login. It is visible to the " +"general public, even if your account is inactive." +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +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 "" + +#: template/account_edit_form.php +msgid "Inactive" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"Please ensure you correctly entered your email address, otherwise you will " +"be locked out." +msgstr "" + +#: template/account_edit_form.php +msgid "Hide Email Address" +msgstr "" + +#: template/account_edit_form.php +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 "" + +#: template/account_edit_form.php +msgid "Backup Email Address" +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 "" + +#: template/account_edit_form.php +msgid "" +"Password reset links are always sent to both your primary and your backup " +"email address." +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 "" + +#: template/account_edit_form.php +msgid "Language" +msgstr "" + +#: template/account_edit_form.php +msgid "Timezone" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"If you want to change the password, enter a new password and confirm the new" +" password by entering it again." +msgstr "" + +#: template/account_edit_form.php +msgid "Re-type password" +msgstr "" + +#: template/account_edit_form.php +msgid "" +"The following information is only required if you want to submit packages to" +" the Arch User Repository." +msgstr "" + +#: template/account_edit_form.php +msgid "SSH Public Key" +msgstr "" + +#: template/account_edit_form.php +msgid "Notification settings" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of new comments" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of package updates" +msgstr "" + +#: template/account_edit_form.php +msgid "Notify of ownership changes" +msgstr "" + +#: template/account_edit_form.php +msgid "To confirm the profile changes, please enter your current password:" +msgstr "" + +#: template/account_edit_form.php +msgid "Your current password" +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 "" + +#: template/account_edit_form.php +msgid "Answer" +msgstr "" + +#: template/account_edit_form.php template/pkgbase_details.php +#: template/pkg_details.php +msgid "Update" +msgstr "" + +#: template/account_edit_form.php +msgid "Create" +msgstr "" + +#: template/account_edit_form.php template/search_accounts_form.php +msgid "Reset" +msgstr "" + +#: template/account_search_results.php +msgid "No results matched your search criteria." +msgstr "" + +#: template/account_search_results.php +msgid "Edit Account" +msgstr "" + +#: template/account_search_results.php +msgid "Suspended" +msgstr "" + +#: template/account_search_results.php +msgid "Edit" +msgstr "" + +#: template/account_search_results.php +msgid "Less" +msgstr "" + +#: template/account_search_results.php +msgid "More" +msgstr "" + +#: template/account_search_results.php +msgid "No more results to display." +msgstr "" + +#: template/comaintainers_form.php +#, php-format +msgid "" +"Use this form to add co-maintainers for %s%s%s (one user name per line):" +msgstr "" + +#: template/comaintainers_form.php +msgid "Users" +msgstr "" + +#: template/comaintainers_form.php template/pkg_comment_form.php +msgid "Save" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "Flagged Out-of-Date Comment: %s" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the following reason:" +msgstr "" + +#: template/flag_comment.php +#, php-format +msgid "%s%s%s is not flagged out-of-date." +msgstr "" + +#: template/flag_comment.php +msgid "Return to Details" +msgstr "" + +#: template/footer.php +#, php-format +msgid "Copyright %s 2004-%d aurweb Development Team." +msgstr "" + +#: template/header.php +msgid " My Account" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Package Actions" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View PKGBUILD" +msgstr "" + +#: template/pkgbase_actions.php +msgid "View Changes" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Download snapshot" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Search wiki" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "Flagged out-of-date (%s)" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Flag package out-of-date" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Unflag package" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Remove vote" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Vote for this package" +msgstr "" + +#: template/pkgbase_actions.php scripts/notify.py +msgid "Disable notifications" +msgstr "" + +#: template/pkgbase_actions.php template/pkg_comment_form.php +msgid "Enable notifications" +msgstr "" + +#: template/pkgbase_actions.php +msgid "Manage Co-Maintainers" +msgstr "" + +#: template/pkgbase_actions.php +#, php-format +msgid "%d pending request" +msgid_plural "%d pending requests" +msgstr[0] "" + +#: template/pkgbase_actions.php +msgid "Adopt Package" +msgstr "" + +#: template/pkgbase_details.php +msgid "Package Base Details" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Git Clone URL" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "read-only" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "click to copy" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Keywords" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php +msgid "Submitter" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Maintainer" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Packager" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Votes" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Popularity" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "First Submitted" +msgstr "" + +#: template/pkgbase_details.php template/pkg_details.php +msgid "Last Updated" +msgstr "" + +#: template/pkg_comment_box.php +#, php-format +msgid "Edit comment for: %s" +msgstr "" + +#: template/pkg_comment_box.php template/pkg_comment_form.php +msgid "Add Comment" +msgstr "" + +#: template/pkg_comment_form.php +msgid "" +"Git commit identifiers referencing commits in the AUR package repository and" +" URLs are converted to links automatically." +msgstr "" + +#: template/pkg_comment_form.php +#, php-format +msgid "%sMarkdown syntax%s is partially supported." +msgstr "" + +#: template/pkg_comments.php +msgid "Pinned Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Latest Comments" +msgstr "" + +#: template/pkg_comments.php +msgid "Comments for" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "%s commented on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Anonymous comment on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "Commented on package %s on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "deleted on %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s by %s" +msgstr "" + +#: template/pkg_comments.php +#, php-format +msgid "edited on %s" +msgstr "" + +#: template/pkg_comments.php +msgid "Undelete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Delete comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Pin comment" +msgstr "" + +#: template/pkg_comments.php +msgid "Unpin comment" +msgstr "" + +#: template/pkg_details.php +msgid "Package Details" +msgstr "" + +#: template/pkg_details.php template/pkg_search_form.php +msgid "Package Base" +msgstr "" + +#: template/pkg_details.php template/pkg_search_results.php +msgid "Description" +msgstr "" + +#: template/pkg_details.php +msgid "Upstream URL" +msgstr "" + +#: template/pkg_details.php +msgid "Visit the website for" +msgstr "" + +#: template/pkg_details.php +msgid "Licenses" +msgstr "" + +#: template/pkg_details.php +msgid "Groups" +msgstr "" + +#: template/pkg_details.php +msgid "Conflicts" +msgstr "" + +#: template/pkg_details.php +msgid "Provides" +msgstr "" + +#: template/pkg_details.php +msgid "Replaces" +msgstr "" + +#: template/pkg_details.php +msgid "Dependencies" +msgstr "" + +#: template/pkg_details.php +msgid "Required by" +msgstr "" + +#: template/pkg_details.php +msgid "Sources" +msgstr "" + +#: template/pkgreq_close_form.php +#, php-format +msgid "Use this form to close the request for package base %s%s%s." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "" +"The comments field can be left empty. However, it is highly recommended to " +"add a comment when rejecting a request." +msgstr "" + +#: template/pkgreq_close_form.php +msgid "Reason" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Accepted" +msgstr "" + +#: template/pkgreq_close_form.php template/pkgreq_results.php +#: template/tu_details.php +msgid "Rejected" +msgstr "" + +#: template/pkgreq_form.php +#, php-format +msgid "" +"Use this form to file a request against package base %s%s%s which includes " +"the following packages:" +msgstr "" + +#: template/pkgreq_form.php +msgid "Request type" +msgstr "" + +#: template/pkgreq_form.php +msgid "Deletion" +msgstr "" + +#: template/pkgreq_form.php +msgid "Orphan" +msgstr "" + +#: template/pkgreq_form.php template/pkg_search_results.php +msgid "Merge into" +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 " +"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." +msgstr "" + +#: template/pkgreq_form.php +msgid "" +"By submitting an orphan request, you ask a Trusted User 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 "" + +#: template/pkgreq_results.php +msgid "No requests matched your search criteria." +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "%d package request found." +msgid_plural "%d package requests found." +msgstr[0] "" + +#: template/pkgreq_results.php template/pkg_search_results.php +#, php-format +msgid "Page %d of %d." +msgstr "" + +#: template/pkgreq_results.php +msgid "Package" +msgstr "" + +#: template/pkgreq_results.php +msgid "Filed by" +msgstr "" + +#: template/pkgreq_results.php +msgid "Date" +msgstr "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d day left" +msgid_plural "~%d days left" +msgstr[0] "" + +#: template/pkgreq_results.php +#, php-format +msgid "~%d hour left" +msgid_plural "~%d hours left" +msgstr[0] "" + +#: template/pkgreq_results.php +msgid "<1 hour left" +msgstr "" + +#: template/pkgreq_results.php +msgid "Accept" +msgstr "" + +#: template/pkgreq_results.php +msgid "Locked" +msgstr "" + +#: template/pkgreq_results.php +msgid "Close" +msgstr "" + +#: template/pkgreq_results.php +msgid "Pending" +msgstr "" + +#: template/pkgreq_results.php +msgid "Closed" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name, Description" +msgstr "" + +#: template/pkg_search_form.php +msgid "Name Only" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Name" +msgstr "" + +#: template/pkg_search_form.php +msgid "Exact Package Base" +msgstr "" + +#: template/pkg_search_form.php +msgid "Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "Maintainer, Co-maintainer" +msgstr "" + +#: template/pkg_search_form.php +msgid "All" +msgstr "" + +#: template/pkg_search_form.php +msgid "Flagged" +msgstr "" + +#: template/pkg_search_form.php +msgid "Not Flagged" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Name" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +#: template/tu_details.php template/tu_list.php +msgid "Voted" +msgstr "" + +#: template/pkg_search_form.php +msgid "Last modified" +msgstr "" + +#: template/pkg_search_form.php +msgid "Ascending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Descending" +msgstr "" + +#: template/pkg_search_form.php +msgid "Enter search criteria" +msgstr "" + +#: template/pkg_search_form.php +msgid "Search by" +msgstr "" + +#: template/pkg_search_form.php template/stats/user_table.php +msgid "Out of Date" +msgstr "" + +#: template/pkg_search_form.php template/search_accounts_form.php +msgid "Sort by" +msgstr "" + +#: template/pkg_search_form.php +msgid "Sort order" +msgstr "" + +#: template/pkg_search_form.php +msgid "Per page" +msgstr "" + +#: template/pkg_search_form.php template/pkg_search_results.php +msgid "Go" +msgstr "" + +#: template/pkg_search_form.php +msgid "Orphans" +msgstr "" + +#: template/pkg_search_results.php +msgid "Error retrieving package list." +msgstr "" + +#: template/pkg_search_results.php +msgid "No packages matched your search criteria." +msgstr "" + +#: template/pkg_search_results.php +#, php-format +msgid "%d package found." +msgid_plural "%d packages found." +msgstr[0] "" + +#: template/pkg_search_results.php +msgid "Version" +msgstr "" + +#: template/pkg_search_results.php +#, php-format +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 "" + +#: template/pkg_search_results.php template/tu_details.php +#: template/tu_list.php +msgid "Yes" +msgstr "" + +#: template/pkg_search_results.php +msgid "orphan" +msgstr "" + +#: template/pkg_search_results.php +msgid "Actions" +msgstr "" + +#: template/pkg_search_results.php +msgid "Unflag Out-of-date" +msgstr "" + +#: template/pkg_search_results.php +msgid "Adopt Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Disown Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Delete Packages" +msgstr "" + +#: template/pkg_search_results.php +msgid "Confirm" +msgstr "" + +#: template/search_accounts_form.php +msgid "Any type" +msgstr "" + +#: template/search_accounts_form.php +msgid "Search" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Statistics" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Orphan Packages" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages added in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past 7 days" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages updated in the past year" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Packages never updated" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Registered Users" +msgstr "" + +#: template/stats/general_stats_table.php +msgid "Trusted Users" +msgstr "" + +#: template/stats/updates_table.php +msgid "Recent Updates" +msgstr "" + +#: template/stats/updates_table.php +msgid "more" +msgstr "" + +#: template/stats/user_table.php +msgid "My Statistics" +msgstr "" + +#: template/tu_details.php +msgid "Proposal Details" +msgstr "" + +#: template/tu_details.php +msgid "This vote is still running." +msgstr "" + +#: template/tu_details.php +#, php-format +msgid "Submitted: %s by %s" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "End" +msgstr "" + +#: template/tu_details.php +msgid "Result" +msgstr "" + +#: template/tu_details.php template/tu_list.php +msgid "No" +msgstr "" + +#: template/tu_details.php +msgid "Abstain" +msgstr "" + +#: template/tu_details.php +msgid "Total" +msgstr "" + +#: template/tu_details.php +msgid "Participation" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last Votes by TU" +msgstr "" + +#: template/tu_last_votes_list.php +msgid "Last vote" +msgstr "" + +#: template/tu_last_votes_list.php template/tu_list.php +msgid "No results found." +msgstr "" + +#: template/tu_list.php +msgid "Start" +msgstr "" + +#: template/tu_list.php +msgid "Back" +msgstr "" + +#: scripts/notify.py +msgid "AUR Password Reset" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +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 "" + +#: scripts/notify.py +msgid "Welcome to the Arch User Repository" +msgstr "" + +#: 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 "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Comment for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] added the following comment to {pkgbase} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"If you no longer wish to receive notifications about this package, please go" +" to the package page [2] and select \"{label}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package Update: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "{user} [1] pushed a new commit to {pkgbase} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Out-of-date Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Ownership Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was adopted by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "The package {pkgbase} [1] was disowned by {user} [2]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Co-Maintainer Notification for {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were added to the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "You were removed from the co-maintainer list of {pkgbase} [1]." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "AUR Package deleted: {pkgbase}" +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{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}\"." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "" +"{user} [1] deleted {pkgbase} [2].\n" +"\n" +"You will no longer receive notifications about this package." +msgstr "" + +#: scripts/notify.py +#, python-brace-format +msgid "TU Vote Reminder: Proposal {id}" +msgstr "" + +#: 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 "" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/zh_CN.po b/po/zh_CN.po index 175ca3a2..53d42bc8 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -3,21 +3,24 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: -# leonfeng , 2015-2016 +# Feng Chao , 2015-2016 # dongfengweixiao , 2015 -# Felix Yan , 2014 +# dongfengweixiao , 2015 +# Felix Yan , 2014,2021 +# Feng Chao , 2012,2021 # Lukas Fleischer , 2011 # pingplug , 2017-2018 -# leonfeng , 2012 +# Feng Chao , 2012 # Weiwen Zhao , 2013 # Xiaodong Qi , 2016 +# 林凌儿 <308627244@qq.com>, 2021 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\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" +"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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -39,7 +42,7 @@ msgstr "提示" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "Git 克隆地址并不意味着能被浏览器打开" +msgstr "Git clone URLs 并不意味着能被浏览器打开。" #: html/404.php #, php-format @@ -49,7 +52,7 @@ msgstr "要克隆 Git 项目 %s,运行 %s。" #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "点击 %s这里%s 以回到 %s 的软件包详情页面" +msgstr "点击 %s这里%s 以回到 %s 的软件包详情页面。" #: html/503.php msgid "Service Unavailable" @@ -58,7 +61,7 @@ msgstr "服务不可用" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "别怕!本站正在维护中,我们会回来的。" +msgstr "别慌!本站正在维护中,不久后将恢复。" #: html/account.php msgid "Account" @@ -82,7 +85,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." @@ -207,14 +210,14 @@ msgstr "搜索共同维护的软件包" 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。" +msgstr "欢迎来到 AUR!想了解更多信息,请阅读 %sAUR 用户指南%s 和 %sAUR 受信用户指南%s。" #: html/home.php #, php-format msgid "" "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s " "otherwise they will be deleted!" -msgstr "提交的 PKGBUILD %s必须%s遵守 %sArch 软件包规范%s,否则它们将被删除!" +msgstr "提交的 PKGBUILD %s必须%s 遵守 %sArch 软件包规范%s,否则它们将被删除!" #: html/home.php msgid "Remember to vote for your favourite packages!" @@ -232,7 +235,7 @@ msgstr "免责声明" msgid "" "AUR packages are user produced content. Any use of the provided files is at " "your own risk." -msgstr "AUR 包为用户产生的内容。使用它们造成的后果自负。" +msgstr "AUR 包是用户创作的内容。使用它们造成的后果自负。" #: html/home.php msgid "Learn more..." @@ -240,7 +243,7 @@ msgstr "了解更多..." #: html/home.php msgid "Support" -msgstr "支持" +msgstr "帮助" #: html/home.php msgid "Package Requests" @@ -261,7 +264,7 @@ msgstr "弃置请求" 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 "请求标记这个软件包为无主的,比如维护者不活跃而且这个软件包已经被标记为过时很久了。" +msgstr "请求弃置一个软件包,比如当维护者不活跃且软件包已过期很久时。" #: html/home.php msgid "Deletion Request" @@ -289,7 +292,7 @@ msgstr "请求一个软件包被合并到另一个中。在一个包需要被重 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 "如果你想讨论一个请求,你可以在 %saur-requests%s 邮件列表上发送消息。然而,不要在那里发送请求。" +msgstr "如果您想讨论请求,可以使用 %saur-requests%s 邮件列表。但请不要用它发送请求。" #: html/home.php msgid "Submitting Packages" @@ -330,7 +333,7 @@ msgid "" "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 自身的 Bug。如果想要报告打包方面的 Bug,请联系相应的包维护者,或者在相应的软件包页面中留下一条评论。" +msgstr "如果您在 AUR web 页面发现 Bug,请提交到我们的 %sBug 追踪系统%s。追踪系统 %s只能%s 用来报告 AUR web 页面的 Bug。如果想报告打包方面的 Bug,请联系相应的包维护者,或者在相应的软件包页面留下评论。" #: html/home.php msgid "Package Search" @@ -379,7 +382,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" @@ -397,7 +400,7 @@ msgstr "找回密码" #, php-format msgid "" "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." -msgstr "HTTP 登录已被禁用,请使用%s切换到HTTPS%s。" +msgstr "HTTP 登录已被禁用,想登录请 %s切换到HTTPS%s。" #: html/packages.php template/pkg_search_form.php msgid "Search Criteria" @@ -427,7 +430,7 @@ msgstr "密码至少要 %s 个字符。" #: html/passreset.php msgid "Invalid e-mail." -msgstr "电子邮箱不正确。" +msgstr "邮箱无效。" #: html/passreset.php msgid "Password Reset" @@ -435,15 +438,15 @@ msgstr "密码重置" #: html/passreset.php msgid "Check your e-mail for the confirmation link." -msgstr "请进入您的邮箱查看确认邮件。" +msgstr "请进入您的邮箱查看确认链接。" #: html/passreset.php msgid "Your password has been reset successfully." -msgstr "密码已经成功重置。" +msgstr "您的密码已经成功重置。" #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "确认用户名或主邮件地址:" #: html/passreset.php msgid "Enter your new password:" @@ -462,11 +465,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" @@ -478,6 +481,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 "找不到合并投票与评论的目标包。" @@ -505,11 +514,11 @@ msgstr "删除软件包" msgid "" "Use this form to delete the package base %s%s%s and the following packages " "from the AUR: " -msgstr "使用这个表单将包基础 %s%s%s 和以下包从 AUR 中移除:" +msgstr "使用这个表单将包基础 %s%s%s 和以下包从 AUR 中移除: " #: html/pkgdel.php msgid "Deletion of a package is permanent. " -msgstr "删除软件包是一个永久性的操作" +msgstr "删除软件包是永久性的操作。 " #: html/pkgdel.php html/pkgmerge.php msgid "Select the checkbox to confirm action." @@ -536,29 +545,29 @@ msgstr "弃置软件包" msgid "" "Use this form to disown the package base %s%s%s which includes the following" " packages: " -msgstr "使用这个表格来弃置包基础 %s%s%s,其中包括下列软件包:" +msgstr "使用这个表格来弃置包基础 %s%s%s,其中包括下列软件包: " #: html/pkgdisown.php msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "通过选中复选框,您将确认您不愿成为这个包的共同维护者。" +msgstr "选中复选框以确认您不再是这个包的共同维护者。" #: html/pkgdisown.php #, php-format msgid "" "By selecting the checkbox, you confirm that you want to disown the package " "and transfer ownership to %s%s%s." -msgstr "通过选中复选框,您将确认您将弃置这个包并将其所有权转移给 %s%s%s。" +msgstr "选中复选框以确认您想弃置这个包并将其所有权转移给 %s%s%s。" #: html/pkgdisown.php msgid "" "By selecting the checkbox, you confirm that you want to disown the package." -msgstr "通过选中复选框,您将确认您将弃置这个包。" +msgstr "选中复选框以确认您想弃置这个包。" #: html/pkgdisown.php msgid "Confirm to disown the package" -msgstr "继续以弃置包" +msgstr "确认弃置这个包" #: html/pkgdisown.php msgid "Disown" @@ -576,12 +585,20 @@ 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 "" "Use this form to flag the package base %s%s%s and the following packages " "out-of-date: " -msgstr "使用这个表单将包基础 %s%s%s 和以下包标记为已过期 :" +msgstr "使用这个表单将包基础 %s%s%s 和以下包标记为已过期 : " #: html/pkgflag.php #, php-format @@ -620,19 +637,19 @@ msgstr "合并软件包" #: html/pkgmerge.php #, php-format msgid "Use this form to merge the package base %s%s%s into another package. " -msgstr "使用这个表单将包基础 %s%s%s 合并到另一个包。" +msgstr "使用这个表单将包基础 %s%s%s 合并到另一个包。 " #: html/pkgmerge.php msgid "The following packages will be deleted: " -msgstr "下面这些软件包将被删除:" +msgstr "下面这些软件包将被删除: " #: html/pkgmerge.php msgid "Once the package has been merged it cannot be reversed. " -msgstr "一旦包被合并,这个操作将是不可逆的。" +msgstr "一旦包被合并,这个操作将是不可逆的。 " #: html/pkgmerge.php msgid "Enter the package name you wish to merge the package into. " -msgstr "请输入合并的目标包名。" +msgstr "请输入合并的目标包名。 " #: html/pkgmerge.php msgid "Merge into:" @@ -730,7 +747,7 @@ msgstr "您已经在这个提议上投票了。" #: html/tu.php msgid "Vote ID not valid." -msgstr "非法的投票标识" +msgstr "非法的投票标识。" #: html/tu.php template/tu_list.php msgid "Current Votes" @@ -756,7 +773,7 @@ msgstr "缺少用户标识" #: lib/acctfuncs.inc.php msgid "The username is invalid." -msgstr "用户名需要符合以下条件:" +msgstr "用户名无效。" #: lib/acctfuncs.inc.php #, php-format @@ -769,23 +786,23 @@ msgstr "开头和结尾是数字/英文字母" #: lib/acctfuncs.inc.php msgid "Can contain only one period, underscore or hyphen." -msgstr "最多包含一个“.”,“_”,或“-”" +msgstr "最多包含一个“.”,“_”或“-”。" #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "请确认新密码。" #: lib/acctfuncs.inc.php msgid "The email address is invalid." -msgstr "错误的 Email 地址。" +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." -msgstr "首页无效,请指定完整的 HTTP(s) URL。" +msgstr "主页无效,请指定完整的 HTTP(s) URL。" #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." @@ -793,7 +810,7 @@ msgstr "PGP 密钥指纹无效。" #: lib/acctfuncs.inc.php msgid "The SSH public key is invalid." -msgstr "此 SSH 公钥无效。" +msgstr "SSH 公钥无效。" #: lib/acctfuncs.inc.php msgid "Cannot increase account permissions." @@ -824,15 +841,15 @@ msgstr "SSH 公钥 %s%s%s 已被使用。" #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "验证码未输入。" #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "验证码已过期,请重试。" #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "输入的验证码无效。" #: lib/acctfuncs.inc.php #, php-format @@ -846,11 +863,11 @@ msgstr "帐户 %s%s%s 创建成功。" #: lib/acctfuncs.inc.php msgid "A password reset key has been sent to your e-mail address." -msgstr "一个密码重置密钥已经发送到你的电子邮箱。" +msgstr "密码重置密钥已经发送到您的邮箱。" #: lib/acctfuncs.inc.php msgid "Click on the Login link above to use your account." -msgstr "点击上方的登录链接以使用你的帐号。" +msgstr "点击上方的登录链接以使用您的帐号。" #: lib/acctfuncs.inc.php #, php-format @@ -872,13 +889,17 @@ msgstr "登录表单目前对您所使用的 IP 地址禁用,原因可能是 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 "" "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 %sPassword Reset%s page." -msgstr "你的密码已经成功被重置。如果你刚刚创建了一个新账户,请使用确认邮件中的链接来设置一个初始密码。否则,请在 %s重置密码%s 页面申请一个重置密码密钥。" +msgstr "您的密码已经成功重置。如果您刚刚创建了新账户,请使用确认邮件中的链接来设置初始密码。否则,请在 %s重置密码%s 页面申请重置密钥。" #: lib/acctfuncs.inc.php msgid "Bad username or password." @@ -907,7 +928,7 @@ msgstr "缺少软件包基础 ID 或软件包名。" #: lib/aurjson.class.php lib/pkgbasefuncs.inc.php msgid "You are not allowed to edit this comment." -msgstr "你没有权限编辑评论。" +msgstr "您没有权限编辑这条评论。" #: lib/aurjson.class.php msgid "Comment does not exist." @@ -935,11 +956,11 @@ msgstr "不能同时锁定 5 条以上的评论。" #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to pin this comment." -msgstr "你没有权限锁定这条评论。" +msgstr "您没有权限锁定这条评论。" #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to unpin this comment." -msgstr "你没有权限解锁这条评论。" +msgstr "您没有权限解锁这条评论。" #: lib/pkgbasefuncs.inc.php msgid "Comment has been pinned." @@ -957,6 +978,30 @@ msgstr "获取软件包详情时发生错误。" msgid "Package details could not be found." msgstr "无法找到软件包的详细信息。" +#: 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 msgid "You must be logged in before you can flag packages." msgstr "您需要登录后才能标记软件包。" @@ -993,18 +1038,30 @@ 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 "选中的软件包已经被删除。" +msgstr "选中的软件包已被删除。" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can adopt packages." -msgstr "登录后才能接管软件包" +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 "您没有选择要接管的软件包。" @@ -1057,7 +1114,7 @@ msgstr "您将不再收到 %s 的评论通知。" #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to undelete this comment." -msgstr "你没有权限恢复这条评论。" +msgstr "您没有权限恢复这条评论。" #: lib/pkgbasefuncs.inc.php msgid "Comment has been undeleted." @@ -1077,7 +1134,7 @@ msgstr "评论已编辑。" #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to edit the keywords of this package base." -msgstr "你没有权限编辑这个软件包基础的关键词。" +msgstr "您没有权限编辑这个软件包基础的关键词。" #: lib/pkgbasefuncs.inc.php msgid "The package base keywords have been updated." @@ -1085,12 +1142,12 @@ msgstr "软件包基础的关键词已被更新。" #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to manage co-maintainers of this package base." -msgstr "你没有权限管理这个软件包基础的共同管理者。" +msgstr "您没有权限管理这个软件包基础的共同维护者。" #: lib/pkgbasefuncs.inc.php #, php-format msgid "Invalid user name: %s" -msgstr "非法用户名:%s" +msgstr "用户名无效:%s" #: lib/pkgbasefuncs.inc.php msgid "The package base co-maintainers have been updated." @@ -1111,7 +1168,7 @@ msgstr "登录之后才能提交软件包需求。" #: lib/pkgreqfuncs.inc.php msgid "Invalid name: only lowercase letters are allowed." -msgstr "无效的名称: 只允许小写字母。" +msgstr "名称无效: 只允许小写字母。" #: lib/pkgreqfuncs.inc.php msgid "The comment field must not be empty." @@ -1119,7 +1176,7 @@ msgstr "评论内容不能为空。" #: lib/pkgreqfuncs.inc.php msgid "Invalid request type." -msgstr "无效的请求类型。" +msgstr "请求类型无效。" #: lib/pkgreqfuncs.inc.php msgid "Added request successfully." @@ -1127,7 +1184,7 @@ msgstr "请求提交成功。" #: lib/pkgreqfuncs.inc.php msgid "Invalid reason." -msgstr "无效的理由。" +msgstr "理由无效。" #: lib/pkgreqfuncs.inc.php msgid "Only TUs and developers can close requests." @@ -1140,12 +1197,12 @@ 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 msgid "%sWARNING%s: This action cannot be undone." -msgstr "%s警告%s:这个操作无法被撤销" +msgstr "%s警告%s:这个操作无法撤销。" #: template/account_delete.php msgid "Confirm deletion" @@ -1245,22 +1302,22 @@ msgstr "编辑此用户的帐号" #: template/account_details.php msgid "List this user's comments" -msgstr "" +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 msgid "Click %shere%s for user details." -msgstr "点击 %s这里%s 访问用户详情页面" +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" @@ -1303,30 +1360,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" @@ -1340,7 +1397,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" @@ -1350,7 +1407,7 @@ msgstr "确认密码" msgid "" "The following information is only required if you want to submit packages to" " the Arch User Repository." -msgstr "仅仅当你想要向 AUR 提交软件包时才需要填写以下信息。" +msgstr "仅当你想向 AUR 提交软件包时才需要填写以下信息。" #: template/account_edit_form.php msgid "SSH Public Key" @@ -1374,21 +1431,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 @@ -1435,7 +1492,7 @@ msgstr "没有更多的结果供显示。" #, php-format msgid "" "Use this form to add co-maintainers for %s%s%s (one user name per line):" -msgstr "使用这个表单来添加 %s%s%s 的共同维护者(一个用户名一行):" +msgstr "使用这个表单以添加 %s%s%s 的共同维护者(一个用户名一行):" #: template/comaintainers_form.php msgid "Users" @@ -1467,11 +1524,11 @@ msgstr "返回详情" #: template/footer.php #, php-format msgid "Copyright %s 2004-%d aurweb Development Team." -msgstr "版权所有 %s 2004-%d aurweb 开发组" +msgstr "版权所有 %s 2004-%d aurweb 开发组。" #: template/header.php msgid " My Account" -msgstr "我的帐户" +msgstr " 我的帐户" #: template/pkgbase_actions.php msgid "Package Actions" @@ -1542,7 +1599,7 @@ msgstr "包基础详情" #: template/pkgbase_details.php template/pkg_details.php msgid "Git Clone URL" -msgstr "Git 克隆地址" +msgstr "Git Clone URL" #: template/pkgbase_details.php template/pkg_details.php msgid "read-only" @@ -1550,7 +1607,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 @@ -1560,7 +1617,7 @@ msgstr "关键字" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Submitter" -msgstr "提交人" +msgstr "提交者" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php @@ -1569,7 +1626,7 @@ msgstr "维护者" #: template/pkgbase_details.php template/pkg_details.php msgid "Last Packager" -msgstr "最近一次打包者" +msgstr "最后打包者" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php @@ -1602,12 +1659,12 @@ msgstr "添加评论" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "在AUR软件包仓库内,Git提交和识别符将会被自动转换为URL。" #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "部分支持%sMarkdown语法%s" #: template/pkg_comments.php msgid "Pinned Comments" @@ -1615,11 +1672,11 @@ msgstr "已锁定评论" #: template/pkg_comments.php msgid "Latest Comments" -msgstr "最新的评论" +msgstr "最新评论" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "评论" #: template/pkg_comments.php #, php-format @@ -1634,7 +1691,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 @@ -1714,15 +1771,15 @@ msgstr "取代" #: template/pkg_details.php msgid "Dependencies" -msgstr "依赖于:" +msgstr "依赖" #: template/pkg_details.php msgid "Required by" -msgstr "被需要:" +msgstr "被需要" #: template/pkg_details.php msgid "Sources" -msgstr "源代码:" +msgstr "源代码" #: template/pkgreq_close_form.php #, php-format @@ -2162,7 +2219,7 @@ msgstr "AUR 软件包更新:{pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "用户 {user} [1] 在软件包 {pkgbase} [2] 上传了新的提交:" +msgstr "用户 {user} [1] 在软件包 {pkgbase} [2] 上传了新的提交。" #: scripts/notify.py #, python-brace-format @@ -2216,7 +2273,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 @@ -2237,3 +2294,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "请记得为提案 {id} [1] 投票,投票时段将于48小时内结束。" + +#: 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 "" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "" + +#: 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 "" diff --git a/po/zh_TW.po b/po/zh_TW.po index 5226940a..e7399a19 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -3,16 +3,16 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: -# byStarTW (pan93412) , 2018 +# pan93412 , 2018 # 黃柏諺 , 2014-2017 -# 黃柏諺 , 2020 +# 黃柏諺 , 2020-2022 msgid "" msgstr "" "Project-Id-Version: aurweb\n" "Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-02-01 03:21+0000\n" -"Last-Translator: 黃柏諺 \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" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -473,6 +473,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 "找不到合併投票與評論的目標套件。" @@ -571,6 +577,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 "這似乎是 VCS 軟體包。請%s不要%s因為 AUR 中的軟體包版本與最新的 commit 不相符就將其標記為過期。僅當來源移動或是因為最新的上游變更而需要變更 PKGBUILD 時才標記此軟體包。" + #: html/pkgflag.php #, php-format msgid "" @@ -867,6 +881,10 @@ msgstr "登入表單在您目前所使用的 IP 位址被停用,可能是因 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 "" @@ -952,6 +970,30 @@ msgstr "擷取套件細節時發生錯誤。" msgid "Package details could not be found." msgstr "找不到套件的詳細資訊。" +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "錯誤的 Referer 標頭。" + +#: 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 msgid "You must be logged in before you can flag packages." msgstr "您必須先登入才能標記套件。" @@ -988,6 +1030,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 +1042,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 "您沒有選擇任何要接管的套件。" @@ -2232,3 +2286,48 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "請記得在第 {id} [1] 號建議投下你的投票,投票期限即將在 48 個小時內結束。" + +#: 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 "您無權變更此使用者的帳號類型至 %s。" + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "沒有要接受的 %s 到期既有孤立請求。" + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "內部伺服器錯誤" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +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 "已記錄詳細資訊,並將會由 postmaster posthaste 審閱。我們對造成如此不便深感抱歉。" + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "AUR 伺服器錯誤" + +#: 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 "" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..db16fedf --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1913 @@ +[[package]] +name = "aiofiles" +version = "0.7.0" +description = "File support for asyncio." +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "alembic" +version = "1.7.5" +description = "A database migration tool for SQLAlchemy." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" + +[package.extras] +tz = ["python-dateutil"] + +[[package]] +name = "anyio" +version = "3.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["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)"] + +[[package]] +name = "asgiref" +version = "3.4.1" +description = "ASGI specs, helper code, and adapters" +category = "main" +optional = false +python-versions = ">=3.6" + +[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"] + +[[package]] +name = "authlib" +version = "0.15.5" +description = "The ultimate Python library in building OAuth and OpenID Connect servers." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +cryptography = "*" + +[package.extras] +client = ["requests"] + +[[package]] +name = "bcrypt" +version = "3.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" + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "bleach" +version = "4.1.0" +description = "An easy safelist-based HTML-sanitizing tool." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +packaging = "*" +six = ">=1.9.0" +webencodings = "*" + +[[package]] +name = "certifi" +version = "2021.10.8" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "charset-normalizer" +version = "2.0.10" +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"] + +[[package]] +name = "click" +version = "8.0.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "36.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[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)"] +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)"] + +[[package]] +name = "dnspython" +version = "2.1.0" +description = "DNS toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +dnssec = ["cryptography (>=2.6)"] +doh = ["requests", "requests-toolbelt"] +idna = ["idna (>=2.1)"] +curio = ["curio (>=1.2)", "sniffio (>=1.1)"] +trio = ["trio (>=0.14.0)", "sniffio (>=1.1)"] + +[[package]] +name = "dunamai" +version = "1.7.0" +description = "Dynamic version generation" +category = "main" +optional = false +python-versions = ">=3.5,<4.0" + +[package.dependencies] +packaging = ">=20.9" + +[[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" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +dnspython = ">=1.15.0" +idna = ">=2.0.0" + +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + +[[package]] +name = "fakeredis" +version = "1.7.0" +description = "Fake implementation of redis API for testing purposes." +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +packaging = "*" +redis = "<4.1.0" +six = ">=1.12" +sortedcontainers = "*" + +[package.extras] +aioredis = ["aioredis"] +lua = ["lupa"] + +[[package]] +name = "fastapi" +version = "0.71.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[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" + +[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)"] + +[[package]] +name = "feedgen" +version = "0.9.0" +description = "Feed Generator (ATOM, RSS, Podcasts)" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +lxml = "*" +python-dateutil = "*" + +[[package]] +name = "filelock" +version = "3.4.2" +description = "A platform independent file lock." +category = "main" +optional = false +python-versions = ">=3.7" + +[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)"] + +[[package]] +name = "flake8" +version = "4.0.1" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.8.0,<2.9.0" +pyflakes = ">=2.4.0,<2.5.0" + +[[package]] +name = "greenlet" +version = "1.1.2" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + +[[package]] +name = "gunicorn" +version = "20.1.0" +description = "WSGI HTTP Server for UNIX" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +eventlet = ["eventlet (>=0.24.1)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +tornado = ["tornado (>=0.2)"] + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "h2" +version = "4.1.0" +description = "HTTP/2 State-Machine based protocol implementation" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +hpack = ">=4.0,<5" +hyperframe = ">=6.0,<7" + +[[package]] +name = "hpack" +version = "4.0.0" +description = "Pure-Python HPACK header compression" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "httpcore" +version = "0.13.7" +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" + +[package.dependencies] +certifi = "*" +charset-normalizer = "*" +httpcore = ">=0.13.3,<0.14.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +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)"] +http2 = ["h2 (>=3,<5)"] + +[[package]] +name = "hypercorn" +version = "0.11.2" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +h11 = "*" +h2 = ">=3.1.0" +priority = "*" +toml = "*" +wsproto = ">=0.14.0" + +[package.extras] +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"] + +[[package]] +name = "hyperframe" +version = "6.0.1" +description = "HTTP/2 framing layer for Python" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "importlib-metadata" +version = "4.10.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +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)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "main" +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"] + +[[package]] +name = "itsdangerous" +version = "2.0.1" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "jinja2" +version = "3.0.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lxml" +version = "4.7.1" +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.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + +[[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" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["babel"] +lingua = ["lingua"] + +[[package]] +name = "markdown" +version = "3.3.6" +description = "Python implementation of Markdown." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markupsafe" +version = "2.0.1" +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 = "*" + +[[package]] +name = "mysqlclient" +version = "2.1.0" +description = "Python interface to MySQL" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "orjson" +version = "3.6.5" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "packaging" +version = "21.3" +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" + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "parse" +version = "1.19.0" +description = "parse() is the opposite of format()" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=3.6" + +[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" +description = "POSIX IPC primitives (semaphores, shared memory and message queues) for Python" +category = "main" +optional = false +python-versions = "*" + +[[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" + +[[package]] +name = "prometheus-client" +version = "0.12.0" +description = "Python client for the Prometheus monitoring system." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +twisted = ["twisted"] + +[[package]] +name = "prometheus-fastapi-instrumentator" +version = "5.7.1" +description = "Instrument your FastAPI with Prometheus metrics" +category = "main" +optional = false +python-versions = ">=3.6.0,<4.0.0" + +[package.dependencies] +fastapi = ">=0.38.1,<1.0.0" +prometheus-client = ">=0.8.0,<1.0.0" + +[[package]] +name = "protobuf" +version = "3.19.1" +description = "Protocol Buffers" +category = "main" +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.*" + +[[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.*" + +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pydantic" +version = "1.9.0" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyflakes" +version = "2.4.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygit2" +version = "1.7.2" +description = "Python bindings for libgit2." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cffi = ">=1.4.0" + +[[package]] +name = "pyparsing" +version = "3.0.6" +description = "Python parsing module" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.16.0" +description = "Pytest support for asyncio." +category = "dev" +optional = false +python-versions = ">= 3.6" + +[package.dependencies] +pytest = ">=5.4.0" + +[package.extras] +testing = ["coverage", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[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" + +[[package]] +name = "pytest-tap" +version = "3.3" +description = "Test Anything Protocol (TAP) reporting plugin for pytest" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pytest = ">=3.0" +"tap.py" = ">=3.0,<4.0" + +[[package]] +name = "pytest-xdist" +version = "2.5.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "python-multipart" +version = "0.0.5" +description = "A streaming multipart parser for Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.4.0" + +[[package]] +name = "redis" +version = "3.5.3" +description = "Python client for Redis key-value store" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +hiredis = ["hiredis (>=0.1.3)"] + +[[package]] +name = "requests" +version = "2.27.1" +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.*" + +[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" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[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.*" + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "sqlalchemy" +version = "1.4.29" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[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"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +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)"] +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"] + +[[package]] +name = "srcinfo" +version = "0.0.8" +description = "A small library to parse .SRCINFO files" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +parse = "*" + +[[package]] +name = "starlette" +version = "0.17.1" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +anyio = ">=3.0.0,<4" + +[package.extras] +full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] + +[[package]] +name = "tap.py" +version = "3.1" +description = "Test Anything Protocol (TAP) tools" +category = "dev" +optional = false +python-versions = "*" + +[package.extras] +yaml = ["more-itertools", "PyYAML (>=5.1)"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.0" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tomlkit" +version = "0.8.0" +description = "Style preserving TOML library" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + +[[package]] +name = "typing-extensions" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "urllib3" +version = "1.26.7" +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" + +[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)"] + +[[package]] +name = "uvicorn" +version = "0.15.0" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +asgiref = ">=3.4.0" +click = ">=7.0" +h11 = ">=0.8" + +[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)"] + +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "werkzeug" +version = "2.0.2" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +watchdog = ["watchdog"] + +[[package]] +name = "wsproto" +version = "1.0.0" +description = "WebSockets state-machine based protocol implementation" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[[package]] +name = "zipp" +version = "3.7.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[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"] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.9,<3.11" +content-hash = "492c523ad53b760b6132ba644471f9424e5cf7a7bcdd47ba648aa80d1494038f" + +[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.5-py3-none-any.whl", hash = "sha256:a9dde941534e3d7573d9644e8ea62a2953541e27bc1793e166f60b777ae098b4"}, + {file = "alembic-1.7.5.tar.gz", hash = "sha256:7c328694a2e68f03ee971e63c3bd885846470373a5b532cf2c9f1601c413b153"}, +] +anyio = [ + {file = "anyio-3.4.0-py3-none-any.whl", hash = "sha256:2855a9423524abcdd652d942f8932fda1735210f77a6b392eafd9ff34d3fe020"}, + {file = "anyio-3.4.0.tar.gz", hash = "sha256:24adc69309fb5779bc1e06158e143e0b6d2c56b302a3ac3de3083c705a6ed39d"}, +] +asgiref = [ + {file = "asgiref-3.4.1-py3-none-any.whl", hash = "sha256:ffc141aa908e6f175673e7b1b3b7af4fdb0ecb738fc5c8b88f69f055c2415214"}, + {file = "asgiref-3.4.1.tar.gz", hash = "sha256:4ef1ab46b484e3c706329cedeff284a5d40824200638503f5768edb6de7d58e9"}, +] +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.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, + {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, +] +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.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, + {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, + {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, + {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, + {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, + {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, + {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, + {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, + {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, + {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, + {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, + {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, + {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, + {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, + {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, + {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, + {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, + {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, + {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, + {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, +] +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.1.0-py3-none-any.whl", hash = "sha256:95d12f6ef0317118d2a1a6fc49aac65ffec7eb8087474158f42f26a639135216"}, + {file = "dnspython-2.1.0.zip", hash = "sha256:e4a87f0b573201a0f3727fa18a516b055fd1107e0e5477cded4a2de497df1dd4"}, +] +dunamai = [ + {file = "dunamai-1.7.0-py3-none-any.whl", hash = "sha256:375e017eb014681e9c8f6e7f2c4c2065ef35832d429f8b70900bed24e8be83f8"}, + {file = "dunamai-1.7.0.tar.gz", hash = "sha256:6abfeb91768caea59d65a4989cec49472fa66ee04dcd6a5c9f92ebc019926a93"}, +] +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.0-py3-none-any.whl", hash = "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"}, + {file = "importlib_metadata-4.10.0.tar.gz", hash = "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6"}, +] +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.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:6c444edc073eb69cf85b28851a7a957807a41ce9bb3a9c14eefa8b33030cf050"}, + {file = "orjson-3.6.5-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:432c6da3d8d4630739f5303dcc45e8029d357b7ff8e70b7239be7bd047df6b19"}, + {file = "orjson-3.6.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:0fa32319072fadf0732d2c1746152f868a1b0f83c8cce2cad4996f5f3ca4e979"}, + {file = "orjson-3.6.5-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:0d65cc67f2e358712e33bc53810022ef5181c2378a7603249cd0898aa6cd28d4"}, + {file = "orjson-3.6.5-cp310-none-win_amd64.whl", hash = "sha256:fa8e3d0f0466b7d771a8f067bd8961bc17ca6ea4c89a91cd34d6648e6b1d1e47"}, + {file = "orjson-3.6.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:470596fbe300a7350fd7bbcf94d2647156401ab6465decb672a00e201af1813a"}, + {file = "orjson-3.6.5-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d2680d9edc98171b0c59e52c1ed964619be5cb9661289c0dd2e667773fa87f15"}, + {file = "orjson-3.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:001962a334e1ab2162d2f695f2770d2383c7ffd2805cec6dbb63ea2ad96bf0ad"}, + {file = "orjson-3.6.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:522c088679c69e0dd2c72f43cd26a9e73df4ccf9ed725ac73c151bbe816fe51a"}, + {file = "orjson-3.6.5-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:d2b871a745a64f72631b633271577c99da628a9b63e10bd5c9c20706e19fe282"}, + {file = "orjson-3.6.5-cp37-none-win_amd64.whl", hash = "sha256:51ab01fed3b3e21561f21386a2f86a0415338541938883b6ca095001a3014a3e"}, + {file = "orjson-3.6.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:fc7e62edbc7ece95779a034d9e206d7ba9e2b638cc548fd3a82dc5225f656625"}, + {file = "orjson-3.6.5-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:0720d60db3fa25956011a573274a269eb37de98070f3bc186582af1222a2d084"}, + {file = "orjson-3.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169a8876aed7a5bff413c53257ef1fa1d9b68c855eb05d658c4e73ed8dff508"}, + {file = "orjson-3.6.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:331f9a3bdba30a6913ad1d149df08e4837581e3ce92bf614277d84efccaf796f"}, + {file = "orjson-3.6.5-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:ece5dfe346b91b442590a41af7afe61df0af369195fed13a1b29b96b1ba82905"}, + {file = "orjson-3.6.5-cp38-none-win_amd64.whl", hash = "sha256:6a5e9eb031b44b7a429c705ca48820371d25b9467c9323b6ae7a712daf15fbef"}, + {file = "orjson-3.6.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:206237fa5e45164a678b12acc02aac7c5b50272f7f31116e1e08f8bcaf654f93"}, + {file = "orjson-3.6.5-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:d5aceeb226b060d11ccb5a84a4cfd760f8024289e3810ec446ef2993a85dbaca"}, + {file = "orjson-3.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80dba3dbc0563c49719e8cc7d1568a5cf738accfcd1aa6ca5e8222b57436e75e"}, + {file = "orjson-3.6.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:443f39bc5e7966880142430ce091e502aea068b38cb9db5f1ffdcfee682bc2d4"}, + {file = "orjson-3.6.5-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:a06f2dd88323a480ac1b14d5829fb6cdd9b0d72d505fabbfbd394da2e2e07f6f"}, + {file = "orjson-3.6.5-cp39-none-win_amd64.whl", hash = "sha256:82cb42dbd45a3856dbad0a22b54deb5e90b2567cdc2b8ea6708e0c4fe2e12be3"}, + {file = "orjson-3.6.5.tar.gz", hash = "sha256:eb3a7d92d783c89df26951ef3e5aca9d96c9c6f2284c752aa3382c736f950597"}, +] +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.12.0-py2.py3-none-any.whl", hash = "sha256:317453ebabff0a1b02df7f708efbab21e3489e7072b61cb6957230dd004a0af0"}, + {file = "prometheus_client-0.12.0.tar.gz", hash = "sha256:1b12ba48cee33b9b0b9de64a1047cbd3c5f2d0ab6ebcead7ddda613a750ec3c5"}, +] +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.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d80f80eb175bf5f1169139c2e0c5ada98b1c098e2b3c3736667f28cbbea39fc8"}, + {file = "protobuf-3.19.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a529e7df52204565bcd33738a7a5f288f3d2d37d86caa5d78c458fa5fabbd54d"}, + {file = "protobuf-3.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28ccea56d4dc38d35cd70c43c2da2f40ac0be0a355ef882242e8586c6d66666f"}, + {file = "protobuf-3.19.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8b30a7de128c46b5ecb343917d9fa737612a6e8280f440874e5cc2ba0d79b8f6"}, + {file = "protobuf-3.19.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5935c8ce02e3d89c7900140a8a42b35bc037ec07a6aeb61cc108be8d3c9438a6"}, + {file = "protobuf-3.19.1-cp36-cp36m-win32.whl", hash = "sha256:74f33edeb4f3b7ed13d567881da8e5a92a72b36495d57d696c2ea1ae0cfee80c"}, + {file = "protobuf-3.19.1-cp36-cp36m-win_amd64.whl", hash = "sha256:038daf4fa38a7e818dd61f51f22588d61755160a98db087a046f80d66b855942"}, + {file = "protobuf-3.19.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e51561d72efd5bd5c91490af1f13e32bcba8dab4643761eb7de3ce18e64a853"}, + {file = "protobuf-3.19.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:6e8ea9173403219239cdfd8d946ed101f2ab6ecc025b0fda0c6c713c35c9981d"}, + {file = "protobuf-3.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db3532d9f7a6ebbe2392041350437953b6d7a792de10e629c1e4f5a6b1fe1ac6"}, + {file = "protobuf-3.19.1-cp37-cp37m-win32.whl", hash = "sha256:615b426a177780ce381ecd212edc1e0f70db8557ed72560b82096bd36b01bc04"}, + {file = "protobuf-3.19.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d8919368410110633717c406ab5c97e8df5ce93020cfcf3012834f28b1fab1ea"}, + {file = "protobuf-3.19.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:71b0250b0cfb738442d60cab68abc166de43411f2a4f791d31378590bfb71bd7"}, + {file = "protobuf-3.19.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:3cd0458870ea7d1c58e948ac8078f6ba8a7ecc44a57e03032ed066c5bb318089"}, + {file = "protobuf-3.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:655264ed0d0efe47a523e2255fc1106a22f6faab7cc46cfe99b5bae085c2a13e"}, + {file = "protobuf-3.19.1-cp38-cp38-win32.whl", hash = "sha256:b691d996c6d0984947c4cf8b7ae2fe372d99b32821d0584f0b90277aa36982d3"}, + {file = "protobuf-3.19.1-cp38-cp38-win_amd64.whl", hash = "sha256:e7e8d2c20921f8da0dea277dfefc6abac05903ceac8e72839b2da519db69206b"}, + {file = "protobuf-3.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fd390367fc211cc0ffcf3a9e149dfeca78fecc62adb911371db0cec5c8b7472d"}, + {file = "protobuf-3.19.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:d83e1ef8cb74009bebee3e61cc84b1c9cd04935b72bca0cbc83217d140424995"}, + {file = "protobuf-3.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d90676d6f426718463fe382ec6274909337ca6319d375eebd2044e6c6ac560"}, + {file = "protobuf-3.19.1-cp39-cp39-win32.whl", hash = "sha256:e7b24c11df36ee8e0c085e5b0dc560289e4b58804746fb487287dda51410f1e2"}, + {file = "protobuf-3.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:77d2fadcf369b3f22859ab25bd12bb8e98fb11e05d9ff9b7cd45b711c719c002"}, + {file = "protobuf-3.19.1-py2.py3-none-any.whl", hash = "sha256:e813b1c9006b6399308e917ac5d298f345d95bb31f46f02b60cd92970a9afa17"}, + {file = "protobuf-3.19.1.tar.gz", hash = "sha256:62a8e4baa9cb9e064eb62d1002eca820857ab2138440cb4b3ea4243830f94ca7"}, +] +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.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, +] +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.29-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:da64423c05256f4ab8c0058b90202053b201cbe3a081f3a43eb590cd554395ab"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0fc4eec2f46b40bdd42112b3be3fbbf88e194bcf02950fbb88bcdc1b32f07dc7"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-win32.whl", hash = "sha256:101d2e100ba9182c9039699588e0b2d833c54b3bad46c67c192159876c9f27ea"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27m-win_amd64.whl", hash = "sha256:ceac84dd9abbbe115e8be0c817bed85d9fa639b4d294e7817f9e61162d5f766c"}, + {file = "SQLAlchemy-1.4.29-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:15b65887b6c324cad638c7671cb95985817b733242a7eb69edd7cdf6953be1e0"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:78abc507d17753ed434b6cc0c0693126279723d5656d9775bfcac966a99a899b"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb8c993706e86178ce15a6b86a335a2064f52254b640e7f53365e716423d33f4"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:804e22d5b6165a4f3f019dd9c94bec5687de985a9c54286b93ded9f7846b8c82"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56d9d62021946263d4478c9ca012fbd1805f10994cb615c88e7bfd1ae14604d8"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-win32.whl", hash = "sha256:027f356c727db24f3c75828c7feb426f87ce1241242d08958e454bd025810660"}, + {file = "SQLAlchemy-1.4.29-cp310-cp310-win_amd64.whl", hash = "sha256:debaf09a823061f88a8dee04949814cf7e82fb394c5bca22c780cb03172ca23b"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:dc27dcc6c72eb38be7f144e9c2c4372d35a3684d3a6dd43bd98c1238358ee17c"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4ddd4f2e247128c58bb3dd4489922874afce157d2cff0b2295d67fcd0f22494"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9ce960a1dc60524136cf6f75621588e2508a117e04a6e3eedb0968bd13b8c824"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5919e647e1d4805867ea556ed4967c68b4d8b266059fa35020dbaed8ffdd60f3"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-win32.whl", hash = "sha256:886359f734b95ad1ef443b13bb4518bcade4db4f9553c9ce33d6d04ebda8d44e"}, + {file = "SQLAlchemy-1.4.29-cp36-cp36m-win_amd64.whl", hash = "sha256:e9cc6d844e24c307c3272677982a9b33816aeb45e4977791c3bdd47637a8d810"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:5e9cd33459afa69c88fa648e803d1f1245e3caa60bfe8b80a9595e5edd3bda9c"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeaebceb24b46e884c4ad3c04f37feb178b81f6ce720af19bfa2592ca32fdef7"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e89347d3bd2ef873832b47e85f4bbd810a5e626c5e749d90a07638da100eb1c8"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a717c2e70fd1bb477161c4cc85258e41d978584fbe5522613618195f7e87d9b"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-win32.whl", hash = "sha256:f74d6c05d2d163464adbdfbc1ab85048cc15462ff7d134b8aed22bd521e1faa5"}, + {file = "SQLAlchemy-1.4.29-cp37-cp37m-win_amd64.whl", hash = "sha256:621854dbb4d2413c759a5571564170de45ef37299df52e78e62b42e2880192e1"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:f3909194751bb6cb7c5511dd18bcf77e6e3f0b31604ed4004dffa9461f71e737"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd49d21d1f03c81fbec9080ecdc4486d5ddda67e7fbb75ebf48294465c022cdc"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e5f6959466a42b6569774c257e55f9cd85200d5b0ba09f0f5d8b5845349c5822"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0072f9887aabe66db23f818bbe950cfa1b6127c5cb769b00bcc07935b3adb0ad"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-win32.whl", hash = "sha256:ad618d687d26d4cbfa9c6fa6141d59e05bcdfc60cb6e1f1d3baa18d8c62fef5f"}, + {file = "SQLAlchemy-1.4.29-cp38-cp38-win_amd64.whl", hash = "sha256:878daecb6405e786b07f97e1c77a9cfbbbec17432e8a90c487967e32cfdecb33"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:e027bdf0a4cf6bd0a3ad3b998643ea374d7991bd117b90bf9982e41ceb742941"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5de7adfb91d351f44062b8dedf29f49d4af7cb765be65816e79223a4e31062b"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fbc6e63e481fa323036f305ada96a3362e1d60dd2bfa026cac10c3553e6880e9"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dd0502cb091660ad0d89c5e95a29825f37cde2a5249957838e975871fbffaad"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-win32.whl", hash = "sha256:37b46bfc4af3dc226acb6fa28ecd2e1fd223433dc5e15a2bad62bf0a0cbb4e8b"}, + {file = "SQLAlchemy-1.4.29-cp39-cp39-win_amd64.whl", hash = "sha256:08cfd35eecaba79be930c9bfd2e1f0c67a7e1314355d83a378f9a512b1cf7587"}, + {file = "SQLAlchemy-1.4.29.tar.gz", hash = "sha256:fa2bad14e1474ba649cfc969c1d2ec915dd3e79677f346bbfe08e93ef9020b39"}, +] +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.8.0-py3-none-any.whl", hash = "sha256:b824e3466f1d475b2b5f1c392954c6cb7ea04d64354ff7300dc7c14257dc85db"}, + {file = "tomlkit-0.8.0.tar.gz", hash = "sha256:29e84a855712dfe0e88a48f6d05c21118dbafb283bb2eed614d46f80deb8e9a1"}, +] +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.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, + {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, +] +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"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..c6076829 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,115 @@ +# Poetry build configuration for the aurweb project. +# +# Dependencies: +# * python >= 3.9 +# * pip +# * poetry +# * poetry-dynamic-versioning +# +[tool.poetry] +name = "aurweb" +version = "5.0.0" # Updated via poetry-dynamic-versioning +license = "GPL-2.0-only" +description = "Source code for the Arch User Repository's website" +homepage = "https://aur.archlinux.org" +repository = "https://gitlab.archlinux.org/archlinux/aurweb" +documentation = "https://gitlab.archlinux.org/archlinux/aurweb/-/blob/master/README.md" +keywords = ["aurweb", "aur", "Arch", "Linux"] +authors = [ + "Lucas Fleischer ", + "Eli Schwartz ", + "Kevin Morris " +] +maintainers = [ + "Eli Schwartz " +] +packages = [ + { include = "aurweb" } +] + +[tool.poetry-dynamic-versioning] +enable = true +vcs = "git" + +[build-system] +requires = ["poetry>=1.1.8", "poetry-dynamic-versioning"] +build-backend = "poetry.masonry.api" + +[tool.poetry.urls] +"Repository" = "https://gitlab.archlinux.org/archlinux/aurweb" +"Bug Tracker" = "https://gitlab.archlinux.org/archlinux/aurweb/-/issues" +"Development Mailing List" = "https://lists.archlinux.org/listinfo/aur-dev" +"General Mailing List" = "https://lists.archlinux.org/listinfo/aur-general" +"Request Mailing List" = "https://lists.archlinux.org/listinfo/aur-requests" + +[tool.poetry.dependencies] +python = ">=3.9,<3.11" + +# 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" +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" + +# 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" +pyalpm = "^0.10.6" +fastapi = "^0.71.0" +srcinfo = "^0.0.8" + +[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" + +[tool.poetry.scripts] +aurweb-git-auth = "aurweb.git.auth:main" +aurweb-git-serve = "aurweb.git.serve:main" +aurweb-git-update = "aurweb.git.update:main" +aurweb-aurblup = "aurweb.scripts.aurblup:main" +aurweb-mkpkglists = "aurweb.scripts.mkpkglists:main" +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-usermaint = "aurweb.scripts.usermaint:main" +aurweb-config = "aurweb.scripts.config:main" +aurweb-adduser = "aurweb.scripts.adduser:main" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..9f70a2bd --- /dev/null +++ b/pytest.ini @@ -0,0 +1,16 @@ +[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 + +# Build in coverage and pytest-xdist multiproc testing. +addopts = --cov=aurweb --cov-append --dist load --dist loadfile -n auto + +# Our pytest units are located in the ./test/ directory. +testpaths = test diff --git a/schema/gendummydata.py b/schema/gendummydata.py index 1f3d0476..275b3601 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -10,37 +10,38 @@ usage: gendummydata.py outputfilename.sql # insert these users/packages into the AUR database. # import hashlib -import random -import time -import os -import sys -import io import logging +import os +import random +import sys +import time -LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output +import bcrypt + +LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" -DB_HOST = os.getenv("DB_HOST", "localhost") -DB_NAME = os.getenv("DB_NAME", "AUR") -DB_USER = os.getenv("DB_USER", "aur") -DB_PASS = os.getenv("DB_PASS", "aur") -USER_ID = 5 # Users.ID of first bogus user -PKG_ID = 1 # Packages.ID of first package -MAX_USERS = 300 # how many users to 'register' -MAX_DEVS = .1 # what percentage of MAX_USERS are Developers -MAX_TUS = .2 # what percentage of MAX_USERS are Trusted Users -MAX_PKGS = 900 # how many packages to load -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 +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 +# 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 CATEGORIES_COUNT = 17 # the number of categories from aur-schema -VOTING = (0, .30) # percentage range for package voting -OPEN_PROPOSALS = 5 # number of open trusted user proposals -CLOSE_PROPOSALS = 15 # number of closed trusted user proposals +VOTING = (0, .001) # percentage range for package voting +# number of open trusted user proposals +OPEN_PROPOSALS = int(os.environ.get("OPEN_PROPOSALS", 15)) +# number of closed trusted user 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://") RANDOM_LOCS = ("pub", "release", "files", "downloads", "src") -FORTUNE_FILE = "/usr/share/fortune/cookie" +FORTUNE_FILE = os.environ.get("FORTUNE_FILE", "/usr/share/fortune/cookie") # setup logging logformat = "%(levelname)s: %(message)s" @@ -48,20 +49,20 @@ logging.basicConfig(format=logformat, level=LOG_LEVEL) log = logging.getLogger() if len(sys.argv) != 2: - log.error("Missing output filename argument") - raise SystemExit + log.error("Missing output filename argument") + raise SystemExit(1) # make sure the seed file exists # if not os.path.exists(SEED_FILE): - log.error("Please install the 'words' Arch package") - raise SystemExit + log.error("Please install the 'words' Arch package") + raise SystemExit(1) # make sure comments can be created # if not os.path.exists(FORTUNE_FILE): - log.error("Please install the 'fortune-mod' Arch package") - raise SystemExit + log.error("Please install the 'fortune-mod' Arch package") + raise SystemExit(1) # track what users/package names have been used # @@ -69,21 +70,28 @@ seen_users = {} seen_pkgs = {} user_keys = [] + # some functions to generate random data # def genVersion(): - ver = [] - ver.append("%d" % random.randrange(0,10)) - ver.append("%d" % random.randrange(0,20)) - if random.randrange(0,2) == 0: - ver.append("%d" % random.randrange(0,100)) - return ".".join(ver) + "-%d" % random.randrange(1,11) + ver = [] + ver.append("%d" % random.randrange(0, 10)) + ver.append("%d" % random.randrange(0, 20)) + if random.randrange(0, 2) == 0: + ver.append("%d" % random.randrange(0, 100)) + return ".".join(ver) + "-%d" % random.randrange(1, 11) + + def genCategory(): - return random.randrange(1,CATEGORIES_COUNT) + return random.randrange(1, CATEGORIES_COUNT) + + def genUID(): - return seen_users[user_keys[random.randrange(0,len(user_keys))]] + return seen_users[user_keys[random.randrange(0, len(user_keys))]] + + def genFortune(): - return fortunes[random.randrange(0,len(fortunes))].replace("'", "") + return fortunes[random.randrange(0, len(fortunes))].replace("'", "") # load the words, and make sure there are enough words for users/pkgs @@ -93,43 +101,52 @@ fp = open(SEED_FILE, "r", encoding="utf-8") contents = fp.readlines() fp.close() if MAX_USERS > len(contents): - MAX_USERS = len(contents) + MAX_USERS = len(contents) if MAX_PKGS > len(contents): - MAX_PKGS = len(contents) -if len(contents) - MAX_USERS > MAX_PKGS: - need_dupes = 0 -else: - need_dupes = 1 + MAX_PKGS = len(contents) + +need_dupes = 0 +if not len(contents) - MAX_USERS > MAX_PKGS: + need_dupes = 1 + + +def normalize(unicode_data): + """ 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') + # select random usernames # log.debug("Generating random user names...") user_id = USER_ID while len(seen_users) < MAX_USERS: - user = random.randrange(0, len(contents)) - word = contents[user].replace("'", "").replace(".","").replace(" ", "_") - word = word.strip().lower() - if word not in seen_users: - seen_users[word] = user_id - user_id += 1 + user = random.randrange(0, len(contents)) + word = contents[user].replace("'", "").replace(".", "").replace(" ", "_") + word = normalize(word.strip().lower()) + if word not in seen_users: + seen_users[word] = user_id + user_id += 1 user_keys = list(seen_users.keys()) + # select random package names # log.debug("Generating random package names...") num_pkgs = PKG_ID while len(seen_pkgs) < MAX_PKGS: - pkg = random.randrange(0, len(contents)) - word = contents[pkg].replace("'", "").replace(".","").replace(" ", "_") - word = word.strip().lower() - if not need_dupes: - if word not in seen_pkgs and word not in seen_users: - seen_pkgs[word] = num_pkgs - num_pkgs += 1 - else: - if word not in seen_pkgs: - seen_pkgs[word] = num_pkgs - num_pkgs += 1 + pkg = random.randrange(0, len(contents)) + word = contents[pkg].replace("'", "").replace(".", "").replace(" ", "_") + word = normalize(word.strip().lower()) + if not need_dupes: + if word not in seen_pkgs and word not in seen_users: + seen_pkgs[word] = num_pkgs + num_pkgs += 1 + else: + if word not in seen_pkgs: + seen_pkgs[word] = num_pkgs + num_pkgs += 1 # free up contents memory # @@ -151,32 +168,38 @@ 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: - account_type = random.randrange(1, 4) - if account_type == 3 and not has_devs: - # this will be a dev account - # - 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 - # - trustedusers.append(seen_users[u]) - if len(trustedusers) >= MAX_TUS * MAX_USERS: - has_tus = 1 - else: - # a normal user account - # - pass + account_type = 1 # default to normal user + if not has_devs or not has_tus: + account_type = random.randrange(1, 4) + if account_type == 3 and not has_devs: + # this will be a dev account + # + 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 + # + trustedusers.append(seen_users[u]) + if len(trustedusers) >= MAX_TUS * MAX_USERS: + has_tus = 1 + else: + # a normal user account + # + pass - h = hashlib.new('md5') - h.update(u.encode()); - s = ("INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd)" - " VALUES (%d, %d, '%s', '%s@example.com', '%s');\n") - s = s % (seen_users[u], account_type, u, u, h.hexdigest()) - out.write(s) + # For dummy data, we just use 4 salt rounds. + salt = bcrypt.gensalt(rounds=4).decode() + + # "{salt}{username}" + to_hash = f"{salt}{u}" + + 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 = 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)) @@ -193,123 +216,126 @@ fp.close() log.debug("Creating SQL statements for packages.") count = 0 for p in list(seen_pkgs.keys()): - NOW = int(time.time()) - if count % 2 == 0: - 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))] - if count % 20 == 0: # every so often, there are orphans... - muid = "NULL" + NOW = int(time.time()) + if count % 2 == 0: + 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))] + if count % 20 == 0: # every so often, there are orphans... + muid = "NULL" - uuid = genUID() # the submitter/user + uuid = genUID() # the submitter/user - s = ("INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " + 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 = s % (seen_pkgs[p], p, NOW, NOW, uuid, muid, puid) + out.write(s) - s = ("INSERT INTO Packages (ID, PackageBaseID, Name, Version) VALUES " + 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) + s = s % (seen_pkgs[p], seen_pkgs[p], p, genVersion()) + out.write(s) - count += 1 + count += 1 - # create random comments for this package - # - 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 = s % (seen_pkgs[p], genUID(), genFortune(), now) - out.write(s) + # create random comments for this package + # + 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 = s % (seen_pkgs[p], genUID(), genFortune(), now) + out.write(s) # Cast votes # 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])) - 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)" - " VALUES (%d, %d);\n") - s = s % (seen_users[u], pkg) - pkgvote[pkg] = 1 - if pkg not in track_votes: - track_votes[pkg] = 0 - track_votes[pkg] += 1 - out.write(s) + 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)" + " VALUES (%d, %d);\n") + s = s % (seen_users[u], pkg) + pkgvote[pkg] = 1 + if pkg not in track_votes: + track_votes[pkg] = 0 + track_votes[pkg] += 1 + out.write(s) # Update statements for package votes # for p in list(track_votes.keys()): - s = "UPDATE PackageBases SET NumVotes = %d WHERE ID = %d;\n" - s = s % (track_votes[p], p) - out.write(s) + s = "UPDATE PackageBases SET NumVotes = %d WHERE ID = %d;\n" + s = s % (track_votes[p], p) + out.write(s) # Create package dependencies and sources # log.debug("Creating statements for package depends/sources.") -for p in list(seen_pkgs.keys()): - num_deps = random.randrange(PKG_DEPS[0], PKG_DEPS[1]) - for i in range(0, num_deps): - dep = random.choice([k for k in seen_pkgs]) - deptype = random.randrange(1, 5) - if deptype == 4: - dep += ": for " + random.choice([k for k in seen_pkgs]) - s = "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) VALUES (%d, %d, '%s');\n" - s = s % (seen_pkgs[p], deptype, dep) - out.write(s) +# the keys of seen_pkgs are accessed many times by random.choice, +# so the list has to be created outside the loops to keep it efficient +seen_pkgs_keys = list(seen_pkgs.keys()) +for p in seen_pkgs_keys: + num_deps = random.randrange(PKG_DEPS[0], PKG_DEPS[1]) + for i in range(0, num_deps): + dep = random.choice(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 = s % (seen_pkgs[p], deptype, dep) + out.write(s) - num_rels = random.randrange(PKG_RELS[0], PKG_RELS[1]) - for i in range(0, num_deps): - rel = random.choice([k for k in seen_pkgs]) - reltype = random.randrange(1, 4) - s = "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) VALUES (%d, %d, '%s');\n" - s = s % (seen_pkgs[p], reltype, rel) - out.write(s) + num_rels = random.randrange(PKG_RELS[0], PKG_RELS[1]) + 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 = s % (seen_pkgs[p], reltype, rel) + out.write(s) - num_sources = random.randrange(PKG_SRC[0], PKG_SRC[1]) - for i in range(num_sources): - 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))], - RANDOM_LOCS[random.randrange(0,len(RANDOM_LOCS))], - src_file, genVersion()) - s = "INSERT INTO PackageSources(PackageID, Source) VALUES (%d, '%s');\n" - s = s % (seen_pkgs[p], src) - out.write(s) + num_sources = random.randrange(PKG_SRC[0], PKG_SRC[1]) + for i in range(num_sources): + 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))], + RANDOM_LOCS[random.randrange(0, len(RANDOM_LOCS))], + 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 # log.debug("Creating SQL statements for trusted user proposals.") -count=0 +count = 0 for t in range(0, OPEN_PROPOSALS+CLOSE_PROPOSALS): - now = int(time.time()) - if count < CLOSE_PROPOSALS: - start = now - random.randrange(3600*24*7, 3600*24*21) - end = now - random.randrange(0, 3600*24*7) - else: - start = now - end = now + random.randrange(3600*24, 3600*24*7) - if count % 5 == 0: # Don't make the vote about anyone once in a while - 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") - s = s % (genFortune(), user, start, end, suid) - out.write(s) - count += 1 + now = int(time.time()) + if count < CLOSE_PROPOSALS: + start = now - random.randrange(3600*24*7, 3600*24*21) + end = now - random.randrange(0, 3600*24*7) + else: + start = now + end = now + random.randrange(3600*24, 3600*24*7) + if count % 5 == 0: # Don't make the vote about anyone once in a while + 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") + s = s % (genFortune(), user, start, end, suid) + out.write(s) + count += 1 # close output file # diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..08be9186 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,25 @@ +[pycodestyle] +max-line-length = 127 +ignore = E741, W503 + +[flake8] +max-line-length = 127 +max-complexity = 10 + +# Ignore some unavoidable flake8 warnings; we know this is against +# PEP8, but some of the existing codebase uses `I` variables, +# so specifically silence warnings about it in pre-defined files. +# +# In E741, the 'I', 'O', 'l' are ambiguous variable names. +# Our current implementation uses these variables through HTTP +# and the FastAPI form specification wants them named as such. +# +# 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 + diff --git a/setup.py b/setup.py deleted file mode 100644 index ca26f0d8..00000000 --- a/setup.py +++ /dev/null @@ -1,35 +0,0 @@ -import re -from setuptools import setup, find_packages -import sys - -version = None -with open('web/lib/version.inc.php', 'r') as f: - for line in f.readlines(): - match = re.match(r'^define\("AURWEB_VERSION", "v([0-9.]+)"\);$', line) - if match: - version = match.group(1) - -if not version: - sys.stderr.write('error: Failed to parse version file!') - sys.exit(1) - -setup( - name="aurweb", - version=version, - packages=find_packages(), - entry_points={ - 'console_scripts': [ - 'aurweb-git-auth = aurweb.git.auth:main', - 'aurweb-git-serve = aurweb.git.serve:main', - 'aurweb-git-update = aurweb.git.update:main', - 'aurweb-aurblup = aurweb.scripts.aurblup:main', - 'aurweb-mkpkglists = aurweb.scripts.mkpkglists:main', - '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-usermaint = aurweb.scripts.usermaint:main', - ], - }, -) diff --git a/templates/account/comments.html b/templates/account/comments.html new file mode 100644 index 00000000..8dff53e4 --- /dev/null +++ b/templates/account/comments.html @@ -0,0 +1,31 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{{ "Accounts" | tr }}

+ +
+
+

+ {{ + "Comments for %s%s%s" | tr + | format('' | format(username), + username, + "") + | safe + }} +

+
+ + + + + {% for comment in comments %} + {% include "partials/account/comment.html" %} + {% endfor %} + +
+ +
+ +{% endblock %} diff --git a/templates/account/edit.html b/templates/account/edit.html new file mode 100644 index 00000000..f8895d92 --- /dev/null +++ b/templates/account/edit.html @@ -0,0 +1,46 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{% trans %}Accounts{% endtrans %}

+ + {% if complete %} + + {{ + "The account, %s%s%s, has been successfully modified." + | tr + | format("", user.Username, "") + | safe + }} + + {% else %} + {% if errors %} + {% include "partials/error.html" %} + {% else %} +

+ {{ "Click %shere%s if you want to permanently delete this account." + | tr + | format('' | format(user | account_url), + "") + | safe + }} + {{ "Click %shere%s for user details." + | tr + | format('' | format(user | account_url), + "") + | safe + }} + {{ "Click %shere%s to list the comments made by this account." + | tr + | format('' | format(user | account_url), + "") + | safe + }} +

+ {% endif %} + + {% set form_type = "UpdateAccount" %} + {% include "partials/account_form.html" %} + {% endif %} +
+{% endblock %} diff --git a/templates/account/index.html b/templates/account/index.html new file mode 100644 index 00000000..32b2ca16 --- /dev/null +++ b/templates/account/index.html @@ -0,0 +1,13 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{{ "Accounts" | tr }}

+ + {% if not users %} + {{ "No results matched your search criteria." | tr }} + {% else %} + {% include "partials/account/results.html" %} + {% endif %} +
+{% endblock %} diff --git a/templates/account/search.html b/templates/account/search.html new file mode 100644 index 00000000..d28d4169 --- /dev/null +++ b/templates/account/search.html @@ -0,0 +1,69 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{{ "Accounts" | tr }}

+ {{ "Use this form to search existing accounts." | tr }} +
+
+ +
+{% endblock %} diff --git a/templates/account/show.html b/templates/account/show.html new file mode 100644 index 00000000..26cfbc21 --- /dev/null +++ b/templates/account/show.html @@ -0,0 +1,118 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{% trans %}Accounts{% endtrans %}

+ + {% if not request.user.is_authenticated() %} + {% trans %}You must log in to view user information.{% endtrans %} + {% else %} + + + + + + +
+

{{ user.Username }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if not user.InactivityTS %} + + {% else %} + {% set inactive_ds = user.InactivityTS | dt | as_timezone(timezone) %} + + {% endif %} + + + + + + {% if login_ts %} + + + {% set login_ds = login_ts | dt | as_timezone(timezone) %} + + + {% endif %} + + + + + +
{% trans %}Username{% endtrans %}:{{ user.Username }}
{% trans %}Account Type{% endtrans %}:{{ user.AccountType }}
{% trans %}Email Address{% endtrans %}: + {{ user.Email }} +
{% trans %}Real Name{% endtrans %}:{{ user.RealName }}
{% trans %}Homepage{% endtrans %}: + {% if user.Homepage %} + {{ user.Homepage }} + {% endif %} +
{% trans %}IRC Nick{% endtrans %}:{{ user.IRCNick }}
{% trans %}PGP Key Fingerprint{% endtrans %}:{{ pgp_key }}
{% trans %}Status{% endtrans %}:{{ "Active" | tr }} + {{ + "Inactive since %s" | tr + | format(inactive_ds.strftime("%Y-%m-%d %H:%M")) + }} +
{% trans %}Registration date{% endtrans %}: + {{ user.RegistrationTS.strftime("%Y-%m-%d") }} +
{% trans %}Last Login{% endtrans %}:{{ login_ds.strftime("%Y-%m-%d") }}
{% trans %}Links{% endtrans %}: +
    +
  • + {{ "%sView this user's packages%s" + | tr + | format('' | format(user.Username), "") + | safe + }} +
  • + {% if request.user.can_edit_user(user) %} +
  • + {{ "%sEdit this user's account%s" + | tr + | format('' | format(user.Username), "") + | safe + }} +
  • + {% endif %} + {% if request.user.has_credential(creds.ACCOUNT_LIST_COMMENTS, approved=[user]) %} +
  • + {{ "%sList this user's comments%s" + | tr + | format('' | format(user.Username), "") + | safe + }} +
  • + {% endif %} +
+
+
+ {% endif %} +
+{% endblock %} diff --git a/templates/addvote.html b/templates/addvote.html new file mode 100644 index 00000000..4d2b0292 --- /dev/null +++ b/templates/addvote.html @@ -0,0 +1,68 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% if error %} +

{{ error | e }}

+ {% endif %} + +
+

{{ "Submit a proposal to vote on." | tr }}

+ +
+ +

+ + + {{ "(empty if not applicable)" | tr }} +

+

+ + +

+ +

+
+ +
+ +

+ +
+
+{% endblock %} + diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 00000000..ec998187 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,65 @@ +
+

{{ "Dashboard" | tr }}

+ +

{{ "My Flagged Packages" | tr }}

+ {% if not flagged_packages %} +

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

+ {% else %} + {% with table_id = "flagged-packages", + packages = flagged_packages, + votes = flagged_packages_voted, + notified = flagged_packages_notified + %} + {% include 'partials/packages/results.html' %} + {% endwith %} + {% endif %} + +

{{ "My Requests" | tr }}

+ {% if not package_requests %} +

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

+ {% else %} + {% with requests = package_requests %} + {% include 'partials/packages/requests.html' %} + {% endwith %} + {% endif %} +
+ +
+

{{ "My Packages" | tr }}

+

+ + {{ "Search for packages I maintain" | tr }} + +

+ {% if not packages %} +

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

+ {% else %} + {% with table_id = "my-packages", + votes = packages_voted, + notified = packages_notified + %} + {% include 'partials/packages/results.html' %} + {% endwith %} + {% endif %} +
+ +
+

{{ "Co-Maintained Packages" | tr }}

+

+ + {{ "Search for packages I co-maintain" | tr }} + +

+ {% if not comaintained %} +

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

+ {% else %} + {% with table_id = "comaintained-packages", + packages = comaintained, + votes = comaintained_voted, + notified = comaintained_notified + %} + {% include 'partials/packages/results.html' %} + {% endwith %} + {% endif %} +
+ diff --git a/templates/errors/404.html b/templates/errors/404.html new file mode 100644 index 00000000..d232c656 --- /dev/null +++ b/templates/errors/404.html @@ -0,0 +1,34 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +
+

404 - {% trans %}Page Not Found{% endtrans %}

+

{% trans %}Sorry, the page you've requested does not exist.{% endtrans %}

+ {% if pkgbase %} +
    + {% set pkgname_strong="%s" | format(pkgbase.Name) %} +
  • + {% trans %}Note{% endtrans %}: + {% trans %}Git clone URLs are not meant to be opened in a browser.{% endtrans %} +
  • +
  • + {% set gitcmd="git clone %s" | format(git_clone_uri_anon | format(pkgbase.Name)) %} + {% if is_maintainer %} + {% set gitcmd="git clone %s" | format(git_clone_uri_priv | format(pkgbase.Name)) %} + {% endif %} + {{ + "To clone the Git repository of %s, run %s." + | tr | format(pkgname_strong, gitcmd) | safe + }} +
  • +
  • + {% set pkglink='' | format(pkgbase.Name) %} + {{ + "Click %shere%s to return to the %s details page." + | tr | format(pkglink, "", pkgname_strong) | safe + }} +
  • +
+ {% endif %} +
+{% endblock %} diff --git a/templates/errors/500.html b/templates/errors/500.html new file mode 100644 index 00000000..1a9bfa6b --- /dev/null +++ b/templates/errors/500.html @@ -0,0 +1,20 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

500 - {{ "Internal Server Error" | tr }}

+ +

+ {{ "A fatal error has occurred." | tr }} + {{ + "Details have been logged and will be reviewed by the " + "postmaster posthaste. We apologize for any inconvenience " + "this may have caused." | tr + }} +

+ + {% if config.getboolean("options", "traceback") %} +
{{ traceback }}
+ {% endif %} +
+{% endblock %} diff --git a/templates/errors/503.html b/templates/errors/503.html new file mode 100644 index 00000000..08f737bf --- /dev/null +++ b/templates/errors/503.html @@ -0,0 +1,8 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +
+

503 - {% trans %}Service Unavailable{% endtrans %}

+

{% trans %}Don't panic! This site is down due to maintenance. We will be back soon.{% endtrans %}

+
+{% endblock %} diff --git a/templates/errors/detail.html b/templates/errors/detail.html new file mode 100644 index 00000000..f382a9bb --- /dev/null +++ b/templates/errors/detail.html @@ -0,0 +1,8 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +
+

{{ "%d" | format(exc.status_code) }} - {{ phrase }}

+

{{ exc.detail }}

+
+{% endblock %} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 00000000..a8cae5b8 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,101 @@ +
+

AUR {% trans %}Home{% endtrans %}

+

+ {{ "Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU Guidelines%s for more information." + | tr + | format('', "", + '', "") + | safe + }} + {{ "Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!" + | tr + | format("", "", + '', + "") + | safe + }} + {% trans %}Remember to vote for your favourite packages!{% endtrans %} + {% trans %}Some packages may be provided as binaries in [community].{% endtrans %} +

+

+ {% trans %}DISCLAIMER{% endtrans %}: + {% trans %}AUR packages are user produced content. Any use of the provided files is at your own risk.{% endtrans %} +

+

{% trans %}Learn more...{% endtrans %}

+
+
+

{% 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 package 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 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." + | 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 package maintainer or leave a comment on the appropriate package page." + | tr + | format('', "", + "", "") + | safe + }} +

+
+
+ + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..8b7ad7c2 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,19 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} +
+
+ {% if request.user.is_authenticated() %} + {% include 'dashboard.html' %} + {% else %} + {% include 'home.html' %} + {% endif %} +
+
+
+ {% include 'partials/packages/search_widget.html' %} + {% include 'partials/packages/updates.html' %} + {% include 'partials/packages/statistics.html' %} +
+ +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 00000000..c62de43e --- /dev/null +++ b/templates/login.html @@ -0,0 +1,100 @@ +{% extends 'partials/layout.html' %} + +{% block pageContent %} + +
+

AUR {% trans %}Login{% endtrans %}

+ + {% if request.user.is_authenticated() %} + + {% else %} + {% if request.url.scheme == "http" and config.getboolean("options", "disable_http_login") %} + {% set https_login = url_base.replace("http://", "https://") + "/login" %} +

+ {{ "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." + | tr + | format( + '' | format(https_login), + "") + | safe + }} +

+ {% elif request.user.is_authenticated() %} +

+ {{ "Logged-in as: %s" + | tr + | format("%s" | format(request.user.Username)) + | safe + }} + [{% trans %}Logout{% endtrans %}] +

+ {% else %} +
+
+ {% trans %}Enter login credentials{% endtrans %} + + {% if errors %} +
    + {% for error in errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} + +

+ + + +

+ +

+ + +

+ +

+ + +

+ +

+ + + [{% trans %}Forgot Password{% endtrans %}] + + + +

+ +
+
+ {% endif %} + {% endif %} +
+ +{% endblock %} diff --git a/templates/packages/index.html b/templates/packages/index.html new file mode 100644 index 00000000..6034d2f6 --- /dev/null +++ b/templates/packages/index.html @@ -0,0 +1,98 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% if errors %} + +
    + {% for error in errors %} +
  • {{ error | tr }}
  • + {% endfor %} +
+ {% include "partials/packages/search.html" %} + + {% elif not packages_count %} + + {% include "partials/packages/search.html" %} +
+

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

+
+ + {% if success %} +
    + {% for message in success %} +
  • {{ message | tr }}
  • + {% endfor %} +
+ {% endif %} + + {% else %} + + {% set pages = (packages_count / PP) | ceil %} + {% set page = O / PP %} + + {% if success %} +
    + {% for message in success %} +
  • {{ message | tr }}
  • + {% endfor %} +
+ {% endif %} + + {# Search form #} + {% include "partials/packages/search.html" %} +
+ + {# /packages does things a bit roundabout-wise: + + If SeB is not given, "nd" is the default. + If SB is not given, "p" is the default. + If SO is not given, "d" is the default. + + However, we depend on flipping SO for column sorting. + + This section sets those defaults for the context if + they are not already setup. #} + {% if not SeB %} + {% set SeB = "nd" %} + {% endif %} + {% if not SB %} + {% set SB = "p" %} + {% endif %} + {% if not SO %} + {% set SO = "d" %} + {% endif %} + + {# Pagination widget #} + {% with total = packages_count, + singular = "%d package found.", + plural = "%d packages found.", + prefix = "/packages" %} + {% include "partials/pager.html" %} + {% endwith %} + + {# Package action form: persists query parameters. #} +
+ {# Search results #} + {% with voted = packages_voted, notified = packages_notified %} + {% include "partials/packages/search_results.html" %} + {% endwith %} + + {# Pagination widget #} + {% with total = packages_count, + singular = "%d package found.", + plural = "%d packages found.", + prefix = "/packages" %} + {% include "partials/pager.html" %} + {% endwith %} + + {% if request.user.is_authenticated() %} + {# Package actions #} + {% include "partials/packages/search_actions.html" %} + {% endif %} +
+
+ + {% endif %} +{% endblock %} diff --git a/templates/packages/show.html b/templates/packages/show.html new file mode 100644 index 00000000..25083020 --- /dev/null +++ b/templates/packages/show.html @@ -0,0 +1,24 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% include "partials/packages/search.html" %} +
+

{{ 'Package Details' | tr }}: {{ package.Name }} {{ package.Version }}

+ + {% include "partials/packages/actions.html" %} + + {% set show_package_details = True %} + {% include "partials/packages/details.html" %} + +
+ {% include "partials/packages/package_metadata.html" %} +
+
+ + + + + {% set pkgname = package.Name %} + {% set pkgbase_id = pkgbase.ID %} + {% include "partials/packages/comments.html" %} +{% endblock %} diff --git a/templates/partials/account/comment.html b/templates/partials/account/comment.html new file mode 100644 index 00000000..8c310738 --- /dev/null +++ b/templates/partials/account/comment.html @@ -0,0 +1,40 @@ +{% set header_cls = "comment-header" %} +{% if comment.Deleter %} + {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} +{% endif %} + +{% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %} + + {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} +

+ {{ + "Commented on package %s%s%s on %s%s%s" | tr + | format( + '' | format(comment.PackageBase.Name), + comment.PackageBase.Name, + "", + '' | format( + username, + comment.ID + ), + commented_at.strftime("%Y-%m-%d %H:%M"), + "" + ) | safe + }} + {% if comment.Editor %} + {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} + + ({{ "edited on %s by %s" | tr + | format(edited_on.strftime('%Y-%m-%d %H:%M'), + '%s' | format( + comment.Editor.Username, comment.Editor.Username)) + | safe + }}) + + {% endif %} + + {% include "partials/comment_actions.html" %} +

+ + {% include "partials/comment_content.html" %} +{% endif %} diff --git a/templates/partials/account/results.html b/templates/partials/account/results.html new file mode 100644 index 00000000..1c398ce1 --- /dev/null +++ b/templates/partials/account/results.html @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + {% endfor %} + + +
{{ "Username" | tr }}{{ "Type" | tr }}{{ "Status" | tr }}{{ "Real Name" | tr }}{{ "IRC Nick" | tr }}{{ "PGP Key Fingerprint" | tr }}{{ "Edit Account" | tr }}
+ + {{ user.Username }} + + {{ user.AccountType.AccountType }}{{ "Suspended" if user.Suspended else "Active" }}{{ user.RealName | e }}{{ user.IRCNick | e }}{{ user.PGPKey or '' | e }} + {% if request.user.can_edit_user(user) %} + + {{ "Edit" | tr }} + + {% endif %} +
+ + + + + + +
+
+
+ + {% for k, v in params.items() %} + + {% endfor %} + +
+
+
+
+
+ + {% for k, v in params.items() %} + + {% endfor %} + + +
+
+
+ diff --git a/templates/partials/account_form.html b/templates/partials/account_form.html new file mode 100644 index 00000000..f6a24c66 --- /dev/null +++ b/templates/partials/account_form.html @@ -0,0 +1,353 @@ + +
+
+ +
+
+ +

+ + + + ({% trans %}required{% endtrans %}) +

+

+ {{ "Your user name is the name you will use to login. " + "It is visible to the general public, even if your " + "account is inactive." | tr }} +

+ + {% if request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) %} +

+ + +

+

+ + + +

+ {% endif %} + + {% if request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user]) %} +

+ + +

+ {% endif %} + + +

+ + + + ({% trans %}required{% endtrans %}) +

+

+ {{ "Please ensure you correctly entered your email " + "address, otherwise you will be locked out." | tr }} +

+ + +

+ + + +

+

+ {{ "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." | tr }} +

+ + +

+ + + +

+

+ + {{ "Optionally provide a secondary email address that " + "can be used to restore your account in case you lose " + "access to your primary email address." | tr }} + {{ "Password reset links are always sent to both your " + "primary and your backup email address." | tr }} + {{ "Your backup email address is always only visible to " + "members of the Arch Linux staff, independent of the %s " + "setting." | tr + | format("%s" | format("Hide Email Address" | tr)) + | safe }} + +

+ + +

+ + + +

+ + +

+ + + +

+ + +

+ + + +

+ + +

+ + + +

+ + +

+ + + +

+ + +

+ + + +

+ +
+ + {% if form_type == "UpdateAccount" %} +
+ + {{ + "If you want to change the password, enter a new password " + "and confirm the new password by entering it again." | tr + }} + +

+ + +

+ +

+ + + +

+
+ {% endif %} + +
+ + {{ + "The following information is only required if you " + "want to submit packages to the Arch User Repository." | tr + }} + +

+ + + + +

+
+ +
+ {% trans%}Notification settings{% endtrans %}: +

+ + + +

+

+ + + +

+

+ + + +

+
+ +
+ {% if form_type == "UpdateAccount" %} + + {{ "To confirm the profile changes, please enter " + "your current password:" | tr }} + +

+ + +

+ {% else %} + + + {{ "To protect the AUR against automated account creation, " + "we kindly ask you to provide the output of the following " + "command:" | tr }} + + {{ captcha_salt | captcha_cmdline }} + + +

+ + + ({% trans %}required{% endtrans %}) + + +

+ {% endif %} +
+ +
+

+ + {% if form_type == "UpdateAccount" %} +   + {% else %} +   + {% endif %} + +

+
+
diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html new file mode 100644 index 00000000..98bb1841 --- /dev/null +++ b/templates/partials/archdev-navbar.html @@ -0,0 +1,63 @@ +
+ +
diff --git a/templates/partials/body.html b/templates/partials/body.html new file mode 100644 index 00000000..ccae0fe3 --- /dev/null +++ b/templates/partials/body.html @@ -0,0 +1,10 @@ +
+ {% include 'partials/set_lang.html' %} + {% include 'partials/archdev-navbar.html' %} + + {% block pageContent %} + + {% endblock %} + + {% include 'partials/footer.html' %} +
diff --git a/templates/partials/comment_actions.html b/templates/partials/comment_actions.html new file mode 100644 index 00000000..78c4cc22 --- /dev/null +++ b/templates/partials/comment_actions.html @@ -0,0 +1,100 @@ +{% set pkgbasename = comment.PackageBase.Name %} + +{% if not comment.Deleter %} + {% if request.user.has_credential(creds.COMMENT_DELETE, approved=[comment.User]) %} +
+
+ + +
+
+ {% endif %} + + {% if request.user.has_credential(creds.COMMENT_EDIT, approved=[comment.User]) %} + + {{ 'Edit comment' | tr }} + + + {# Set the edit event listener for this link. We must do this here + so that we can utilize Jinja2's values. #} + + + {% endif %} + + {% if request.user.has_credential(creds.COMMENT_PIN, approved=[comment.PackageBase.Maintainer]) %} + {% if comment.PinnedTS %} +
+
+ + +
+
+ {% else %} +
+
+ + +
+
+ {% endif %} + {% endif %} +{% elif request.user.has_credential(creds.COMMENT_UNDELETE, approved=[comment.User]) %} +
+
+ + +
+
+{% endif %} diff --git a/templates/partials/comment_content.html b/templates/partials/comment_content.html new file mode 100644 index 00000000..f89dc505 --- /dev/null +++ b/templates/partials/comment_content.html @@ -0,0 +1,15 @@ + +{% set article_cls = "article-content" %} +{% if comment.Deleter %} + {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} +{% endif %} + +
+
+ {% if comment.RenderedComment %} + {{ comment.RenderedComment | safe }} + {% else %} + {{ comment.Comments }} + {% endif %} +
+
diff --git a/templates/partials/error.html b/templates/partials/error.html new file mode 100644 index 00000000..6043dfd1 --- /dev/null +++ b/templates/partials/error.html @@ -0,0 +1,15 @@ +{% if errors %} +
    + {% for error in errors %} + {% if error is string %} +
  • {{ error | tr | safe }}
  • + {% elif error is iterable %} +
      + {% for e in error %} +
    • {{ e | tr | safe }}
    • + {% endfor %} +
    + {% endif %} + {% endfor %} +
+{% endif %} diff --git a/templates/partials/footer.html b/templates/partials/footer.html new file mode 100644 index 00000000..3103699f --- /dev/null +++ b/templates/partials/footer.html @@ -0,0 +1,17 @@ + diff --git a/templates/partials/head.html b/templates/partials/head.html new file mode 100644 index 00000000..8bfde020 --- /dev/null +++ b/templates/partials/head.html @@ -0,0 +1,19 @@ + + {% include 'partials/meta.html' %} + + + + + + + + + + + + + + + AUR ({{ language }}) - {{ title | tr }} + diff --git a/templates/partials/layout.html b/templates/partials/layout.html new file mode 100644 index 00000000..68637ed7 --- /dev/null +++ b/templates/partials/layout.html @@ -0,0 +1,10 @@ + + + {% include 'partials/head.html' %} + + + {% include 'partials/navbar.html' %} + {% extends 'partials/body.html' %} + + diff --git a/templates/partials/meta.html b/templates/partials/meta.html new file mode 100644 index 00000000..727100b9 --- /dev/null +++ b/templates/partials/meta.html @@ -0,0 +1 @@ + diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html new file mode 100644 index 00000000..199b2067 --- /dev/null +++ b/templates/partials/navbar.html @@ -0,0 +1,19 @@ + diff --git a/templates/partials/packages/actions.html b/templates/partials/packages/actions.html new file mode 100644 index 00000000..148da592 --- /dev/null +++ b/templates/partials/packages/actions.html @@ -0,0 +1,142 @@ + + diff --git a/templates/partials/packages/comment.html b/templates/partials/packages/comment.html new file mode 100644 index 00000000..1427e0a0 --- /dev/null +++ b/templates/partials/packages/comment.html @@ -0,0 +1,43 @@ +{% set header_cls = "comment-header" %} +{% set article_cls = "article-content" %} +{% if comment.Deleter %} + {% set header_cls = "%s %s" | format(header_cls, "comment-deleted") %} + {% set article_cls = "%s %s" | format(article_cls, "comment-deleted") %} +{% endif %} + +{% if not comment.Deleter or request.user.has_credential(creds.COMMENT_VIEW_DELETED, approved=[comment.Deleter]) %} +

+ {% set commented_at = comment.CommentTS | dt | as_timezone(timezone) %} + {% set view_account_info = 'View account information for %s' | tr | format(comment.User.Username) %} + {{ + "%s commented on %s" | tr | format( + ('%s' | format( + comment.User.Username, + view_account_info, + comment.User.Username + )) if request.user.is_authenticated() else + (comment.User.Username), + '%s' | format( + comment.ID, + commented_at.strftime("%Y-%m-%d %H:%M") + ) + ) + | safe + }} + {% if comment.Editor %} + {% set edited_on = comment.EditedTS | dt | as_timezone(timezone) %} + + ({{ "edited on %s by %s" | tr + | format(edited_on.strftime('%Y-%m-%d %H:%M'), + '%s' | format( + comment.Editor.Username, comment.Editor.Username)) + | safe + }}) + + {% endif %} + + {% include "partials/comment_actions.html" %} +

+ + {% include "partials/comment_content.html" %} +{% endif %} diff --git a/templates/partials/packages/comment_form.html b/templates/partials/packages/comment_form.html new file mode 100644 index 00000000..5368e784 --- /dev/null +++ b/templates/partials/packages/comment_form.html @@ -0,0 +1,47 @@ +{# `action` is assigned the proper route to use for the form action. +When `comment` is provided (PackageComment), we display an edit form +for the comment. Otherwise, we display a new form. + +Routes: + new comment - /pkgbase/{name}/comments + edit comment - /pkgbase/{name}/comments/{id} +#} +{% set action = "/pkgbase/%s/comments" | format(pkgbase.Name) %} +{% if comment %} + {% set action = "/pkgbase/%s/comments/%d" | format(pkgbase.Name, comment.ID) %} +{% endif %} + +
+
+ +

+ {{ "Git commit identifiers referencing commits in the AUR package " + "repository and URLs are converted to links automatically." | tr }} + {{ "%sMarkdown syntax%s is partially supported." | tr + | format('', + "") + | safe }} +

+

+ +

+

+ + {% if comment and not request.user.notified(pkgbase) %} + + + + + {% endif %} +

+
+
diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html new file mode 100644 index 00000000..6e6b9a47 --- /dev/null +++ b/templates/partials/packages/comments.html @@ -0,0 +1,41 @@ + + +{% if request.user.is_authenticated() %} +
+

Add Comment

+ {% include "partials/packages/comment_form.html" %} +
+{% endif %} + +{% if pinned_comments.count() %} +
+
+

+ {{ "Pinned Comments" | tr }} + +

+
+ {% for comment in pinned_comments.all() %} + {% include "partials/packages/comment.html" %} + {% endfor %} +
+{% endif %} + +{% if comments.count() %} +
+
+

+ {{ "Latest Comments" | tr }} + +

+
+ {% 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 new file mode 100644 index 00000000..6dc8ae77 --- /dev/null +++ b/templates/partials/packages/details.html @@ -0,0 +1,161 @@ +{% set pkg = pkgbase.packages.first() %} + + + + + + {% if show_package_details | default(False) %} + + + + + + + + + + + + + {% endif %} + {% if pkgbase.keywords.count() or request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} + + + {% if request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} + + {% else %} + + {% endif %} + + {% endif %} + {% if show_package_details and licenses and licenses.count() %} + + + + + {% endif %} + {% if show_package_details and conflicts and conflicts.count() %} + + + + + {% endif %} + {% if show_package_details and provides and provides.count() %} + + + + + {% endif %} + {% if show_package_details and replaces and replaces.count() %} + + + + + {% endif %} + + + + + + + + + + + + + + + {% if request.user.has_credential(creds.PKGBASE_LIST_VOTERS) %} + + {% else %} + + {% endif %} + + + + + + + {% set submitted = pkgbase.SubmittedTS | dt | as_timezone(timezone) %} + + + + + + {% set updated = pkgbase.ModifiedTS | dt | as_timezone(timezone) %} + + +
{{ "Git Clone URL" | tr }}: + {{ git_clone_uri_anon | format(pkgbase.Name) }} ({{ "read-only" | tr }}, {{ "click to copy" | tr }}) + {% if request.user.is_authenticated() %} +
{{ git_clone_uri_priv | format(pkgbase.Name) }} ({{ "click to copy" | tr }}) + {% endif %} +
{{ "Package Base" | tr }}: + + {{ pkgbase.Name }} + +
{{ "Description" | tr }}:{{ pkg.Description }}
{{ "Upstream URL" | tr }}: + {% if pkg.URL %} + {{ pkg.URL }} + {% else %} + {{ "None" | tr }} + {% endif %} +
{{ "Keywords" | tr }}: +
+
+ + +
+
+
+ {% for keyword in pkgbase.keywords.all() %} + + {{ keyword.Keyword }} + + {% endfor %} +
{{ "Licenses" | tr }}:{{ licenses.all() | join(', ', attribute='License.Name') }}
{{ "Conflicts" | tr }}: + {{ conflicts.all() | join(', ', attribute='RelName') }} +
{{ "Provides" | tr }}: + {{ provides.all() | join(', ', attribute='RelName') }} +
{{ "Replaces" | tr }}: + {{ replaces.all() | join(', ', attribute='RelName') }} +
{{ "Submitter" | tr }}: + {% if request.user.is_authenticated() and pkgbase.Submitter %} + + {{ pkgbase.Submitter.Username }} + + {% else %} + {{ pkgbase.Submitter.Username | default("None" | tr) }} + {% endif %} +
{{ "Maintainer" | tr }}: + {% if request.user.is_authenticated() and pkgbase.Maintainer %} + + {{ pkgbase.Maintainer.Username }} + + {% else %} + {{ pkgbase.Maintainer.Username | default("None" | tr) }} + {% endif %} +
{{ "Last Packager" | tr }}: + {% if request.user.is_authenticated() and pkgbase.Packager %} + + {{ pkgbase.Packager.Username }} + + {% else %} + {{ pkgbase.Packager.Username | default("None" | tr) }} + {% endif %} +
{{ "Votes" | tr }}: + + {{ pkgbase.NumVotes }} + + {{ pkgbase.NumVotes }}
{{ "Popularity" | tr }}:{{ pkgbase.Popularity | number_format(6 if pkgbase.Popularity <= 0.2 else 2) }}
{{ "First Submitted" | tr }}:{{ "%s" | format(submitted.strftime("%Y-%m-%d %H:%M")) }}
{{ "Last Updated" | tr }}:{{ "%s" | format(updated.strftime("%Y-%m-%d %H:%M")) }}
+ + + diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html new file mode 100644 index 00000000..6f58c2be --- /dev/null +++ b/templates/partials/packages/package_metadata.html @@ -0,0 +1,74 @@ +
+

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

+
    + {% for dep in dependencies %} + {# Collect provides for `dep`. #} + {% set provides = dep.provides() %} +
  • + {% set broken = not dep.is_package() %} + {% if broken %} + {% if not provides %} + + {% endif %} + {% else %} + + {% endif %} + {{ dep.DepName }} + {% if broken %} + {% if not provides %} + + {% endif %} + {% else %} + + {% endif %} + + {% if provides %} + ({{ provides | provides_markup | safe }}) + {% endif %} + + {% if dep.DepTypeID == 4 %} + {# If this dependency type is an optdepends (id: 4). #} + {{ dep | dep_extra_desc }} + {% else %} + {{ dep | dep_extra }} + {% endif %} +
  • + {% endfor %} +
+
+ +
+

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

+
    + {% for dep in required_by %} +
  • + + {{ dep.Package.Name }} + + + {% if dep.DepName != package.Name %} + + ({{ "requires %s" | tr | format(dep.DepName) }}) + + {% endif %} + + {{ dep | dep_extra }} +
  • + {% endfor %} +
+
+ +
+

{{ "Sources" | tr }} ({{ sources | length }})

+
+ +
+
    + {% for src in sources %} + {% set file, uri = (src | source_uri) %} +
  • + {{ file }} +
  • + {% endfor %} +
+
diff --git a/templates/partials/packages/pkgbase_metadata.html b/templates/partials/packages/pkgbase_metadata.html new file mode 100644 index 00000000..d4916029 --- /dev/null +++ b/templates/partials/packages/pkgbase_metadata.html @@ -0,0 +1,13 @@ +
+

Packages ({{ packages_count }})

+ +
diff --git a/templates/partials/packages/requests.html b/templates/partials/packages/requests.html new file mode 100644 index 00000000..4084e96f --- /dev/null +++ b/templates/partials/packages/requests.html @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + {% for request in requests %} + {% set requested = request.RequestTS | dt | as_timezone(timezone) %} + + {% if request.PackageBase %} + {# If the PackageBase still exists, link to it. #} + + {% else %} + {# Otherwise, just display PackageBaseName unlinked. #} + + {% endif %} + + + + + + + {% endfor %} + + + +
{{ "Package" | tr }}{{ "Type" | tr }}{{ "Comments" | tr }}{{ "Filed by" | tr }}{{ "Date" | tr }}{{ "Status" | tr }}
+ + {{ request.PackageBaseName }} + + {{ request.PackageBaseName }} + {{ request.RequestType.name_display() | tr }} + {# If the RequestType is a merge and request.MergeBaseName is valid... #} + {% if request.RequestType.ID == 3 and request.MergeBaseName %} + ({{ request.MergeBaseName }}) + {% endif %} + {{ request.Comments }} + + {{ request.User.Username }} + + {{ requested.strftime("%Y-%m-%d %H:%M") }}{{ request.status_display() | tr }}
diff --git a/templates/partials/packages/results.html b/templates/partials/packages/results.html new file mode 100644 index 00000000..a9a206a1 --- /dev/null +++ b/templates/partials/packages/results.html @@ -0,0 +1,57 @@ + + + + + + + + + {% if request.user.is_authenticated() %} + + + {% endif %} + + + + + + {% for pkg in packages %} + {% set flagged = pkg.PackageBase.OutOfDateTS %} + + + {{ pkg.Version }} + + + {% if request.user.is_authenticated() %} + + + {% endif %} + + + + {% endfor %} + +
{{ "Name" | tr }}{{ "Version" | tr }}{{ "Votes" | tr }}{{ "Popularity" | tr }}{{ "Voted" | tr }}{{ "Notify" | tr }}{{ "Description" | tr }}{{ "Maintainer" | tr }}
+ + {{ pkg.Name }} + + {{ pkg.PackageBase.NumVotes }}{{ pkg.PackageBase.Popularity | number_format(2) }} + {# If I voted, display "Yes". #} + {% if pkg.PackageBase.ID in votes %} + {{ "Yes" | tr }} + {% endif %} + + {# If I'm being notified, display "Yes". #} + {% if pkg.PackageBase.ID in notified %} + {{ "Yes" | tr }} + {% endif %} + {{ pkg.Description or '' }} + {% set maintainer = pkg.PackageBase.Maintainer %} + {% if maintainer %} + + {{ maintainer.Username }} + + {% else %} + {{ "orphan" | tr }} + {% endif %} +
diff --git a/templates/partials/packages/search.html b/templates/partials/packages/search.html new file mode 100644 index 00000000..33bc6132 --- /dev/null +++ b/templates/partials/packages/search.html @@ -0,0 +1,73 @@ + diff --git a/templates/partials/packages/search_actions.html b/templates/partials/packages/search_actions.html new file mode 100644 index 00000000..f28e27a9 --- /dev/null +++ b/templates/partials/packages/search_actions.html @@ -0,0 +1,20 @@ +

+ + + + + +

diff --git a/templates/partials/packages/search_results.html b/templates/partials/packages/search_results.html new file mode 100644 index 00000000..680891c4 --- /dev/null +++ b/templates/partials/packages/search_results.html @@ -0,0 +1,113 @@ + + + + {% if request.user.is_authenticated() %} + + {% endif %} + + + + + {% if request.user.is_authenticated() %} + + + {% endif %} + + + + + + {% for pkg in packages %} + {% set flagged = pkg.OutOfDateTS %} + + {% if request.user.is_authenticated() %} + + {% endif %} + + {% if flagged %} + + {% else %} + + {% endif %} + + + {% if request.user.is_authenticated() %} + + + {% endif %} + + + + {% endfor %} + +
+ {% set order = SO %} + {% if SB == "n" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Name" | tr }} + + {{ "Version" | tr }} + {% set order = SO %} + {% if SB == "v" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Votes" | tr }} + + + {% set order = SO %} + {% if SB == "p" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + {{ "Popularity" | tr }}? + + {% set order = SO %} + {% if SB == "w" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Voted" | tr }} + + + {% set order = SO %} + {% if SB == "o" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Notify" | tr }} + + {{ "Description" | tr }} + {% set order = SO %} + {% if SB == "m" %} + {% set order = "d" if order == "a" else "a" %} + {% endif %} + + {{ "Maintainer" | tr }} + +
+ + + + {{ pkg.Name }} + + {{ pkg.Version }}{{ pkg.Version }}{{ pkg.NumVotes }} + {{ pkg.Popularity | number_format(2) }} + + {% if pkg.Voted %} + {{ "Yes" | tr }} + {% endif %} + + {% if pkg.Notify %} + {{ "Yes" | tr }} + {% endif %} + {{ pkg.Description or '' }} + {% if pkg.Maintainer %} + + {{ pkg.Maintainer }} + + {% else %} + {{ "orphan" | tr }} + {% endif %} +
diff --git a/templates/partials/packages/search_widget.html b/templates/partials/packages/search_widget.html new file mode 100644 index 00000000..048e1aa9 --- /dev/null +++ b/templates/partials/packages/search_widget.html @@ -0,0 +1,14 @@ +
+
+
+ + + +
+
+
diff --git a/templates/partials/packages/statistics.html b/templates/partials/packages/statistics.html new file mode 100644 index 00000000..8a6546b9 --- /dev/null +++ b/templates/partials/packages/statistics.html @@ -0,0 +1,55 @@ +
+

{{ "Statistics" | tr }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ "Packages" | tr }}{{ package_count }}
{{ "Orphan Packages" | tr }}{{ orphan_count }}
+ {{ "Packages added in the past 7 days" | tr }} + {{ seven_days_old_added }}
+ {{ "Packages updated in the past 7 days" | tr }} + {{ seven_days_old_updated }}
+ {{ "Packages updated in the past year" | tr }} + {{ year_old_updated }}
+ {{ "Packages never updated" | tr }} + {{ never_updated }}
+ {{ "Registered Users" | tr }} + {{ user_count }}
+ {{ "Trusted Users" | tr }} + {{ trusted_user_count }}
+
+ +{% if request.user.is_authenticated() %} + + {% include 'partials/statistics.html' %} +{% endif %} diff --git a/templates/partials/packages/updates.html b/templates/partials/packages/updates.html new file mode 100644 index 00000000..9b1996e9 --- /dev/null +++ b/templates/partials/packages/updates.html @@ -0,0 +1,35 @@ +
+

+ {{ "Recent Updates" | tr }} + + ({{ "more" | tr }}) + +

+ + RSS Feed + + + RSS Feed + + + + + {% for pkg in package_updates %} + + + + + {% endfor %} + +
+ + {{ pkg.Name }} {{ pkg.Version }} + + + {% set modified = pkg.PackageBase.ModifiedTS | dt | as_timezone(timezone) %} + {{ modified.strftime("%Y-%m-%d %H:%M") }} +
+ +
diff --git a/templates/partials/pager.html b/templates/partials/pager.html new file mode 100644 index 00000000..1adc354b --- /dev/null +++ b/templates/partials/pager.html @@ -0,0 +1,28 @@ +{# A pager widget that can be used for navigation of a number of results. + +Inputs required: + + prefix: Request URI prefix used to produce navigation offsets + singular: Singular sentence to be translated via tn + plural: Plural sentence to be translated via tn + PP: The number of results per page + O: The current offset value + total: The total number of results +#} + +{% set page = ((O / PP) | int) %} +{% set pages = ((total / PP) | ceil) %} + +
+

+ {{ total | tn(singular, plural) | format(total) }} + {% if pages %} + {{ "Page %d of %d." | tr | format(page + 1, pages) }} + {% endif %} +

+ {% if pages > 1 %} +

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

+ {% endif %} +

diff --git a/templates/partials/set_lang.html b/templates/partials/set_lang.html new file mode 100644 index 00000000..e9590050 --- /dev/null +++ b/templates/partials/set_lang.html @@ -0,0 +1,28 @@ +
+
+
+
+ + + + + + + + + +
+
+
+
diff --git a/templates/partials/statistics.html b/templates/partials/statistics.html new file mode 100644 index 00000000..0bf844b6 --- /dev/null +++ b/templates/partials/statistics.html @@ -0,0 +1,27 @@ +
+

{{ "My Statistics" | tr }}

+ + {% set bases = request.user.maintained_bases %} + + + + + + + {% set out_of_date_packages = bases | out_of_date %} + + + + + +
+ + {{ "Packages" | tr }} + + {{ bases.count() }}
+ + {{ "Out of Date" | tr }} + + {{ out_of_date_packages.count() }}
+ +
diff --git a/templates/partials/tu/last_votes.html b/templates/partials/tu/last_votes.html new file mode 100644 index 00000000..94b9c1e8 --- /dev/null +++ b/templates/partials/tu/last_votes.html @@ -0,0 +1,33 @@ +
+

{% trans %}{{ title }}{% endtrans %}

+ + + + + + + + + {% if not votes %} + + + + + {% else %} + {% for vote in votes %} + + + + + {% endfor %} + {% endif %} + +
{{ "User" | tr }}{{ "Last vote" | tr }}
+ {{ "No results found." | tr }} +
{{ vote.User.Username }} + + {{ vote.VoteID }} + +
+ +
diff --git a/templates/partials/tu/proposal/details.html b/templates/partials/tu/proposal/details.html new file mode 100644 index 00000000..fd84f093 --- /dev/null +++ b/templates/partials/tu/proposal/details.html @@ -0,0 +1,109 @@ +

{% trans %}Proposal Details{% endtrans %}

+ +{% if voteinfo.is_running() %} +

+ {% trans %}This vote is still running.{% endtrans %} +

+{% endif %} + + +
+
+ {{ "User" | tr }}: + + {% if voteinfo.User %} + + {{ voteinfo.User }} + + {% else %} + N/A + {% endif %} + +
+ + {% set submitted = voteinfo.Submitted | dt | as_timezone(timezone) %} + {% set submitter = voteinfo.Submitter.Username %} + {% set submitter_uri = "/account/%s" | format(submitter) %} + {% set submitter = '%s' | format(submitter_uri, submitter) %} + {% set end = voteinfo.End | dt | as_timezone(timezone) %} + + +
+ {{ "End" | tr }}: + + {{ end.strftime("%Y-%m-%d %H:%M") }} + +
+ + {% if not voteinfo.is_running() %} +
+ {{ "Result" | tr }}: + {% if not voteinfo.ActiveTUs %} + {{ "unknown" | tr }} + {% elif accepted %} + + {{ "Accepted" | tr }} + + {% else %} + + {{ "Rejected" | tr }} + + {% endif %} +
+ {% endif %} +
+ +
+

+ + {{ voteinfo.Agenda | replace("\n", "
\n") | safe | e }} +

+
+ + + + {% if not voteinfo.is_running() %} + + + + {% endif %} + + + + + + + + {% if not voteinfo.is_running() %} + + + + {% endif %} + + + + + +
{{ "Yes" | tr }}{{ "No" | tr }}{{ "Abstain" | tr }}{{ "Total" | tr }}{{ "Voted" | tr }}{{ "Participation" | tr }}
{{ voteinfo.Yes }}{{ voteinfo.No }}{{ voteinfo.Abstain }}{{ voteinfo.total_votes() }} + {% if not has_voted %} + + {{ "No" | tr }} + + {% else %} + + {{ "Yes" | tr }} + + {% endif %} + + {% if voteinfo.ActiveTUs %} + {{ (participation * 100) | number_format(2) }}% + {% else %} + {{ "unknown" | tr }} + {% endif %} +
diff --git a/templates/partials/tu/proposal/form.html b/templates/partials/tu/proposal/form.html new file mode 100644 index 00000000..d783a622 --- /dev/null +++ b/templates/partials/tu/proposal/form.html @@ -0,0 +1,14 @@ +
+ +
+ + + +
+
diff --git a/templates/partials/tu/proposal/voters.html b/templates/partials/tu/proposal/voters.html new file mode 100644 index 00000000..6069f97d --- /dev/null +++ b/templates/partials/tu/proposal/voters.html @@ -0,0 +1,10 @@ +

{{ "Voters" | tr }}

+ diff --git a/templates/partials/tu/proposals.html b/templates/partials/tu/proposals.html new file mode 100644 index 00000000..4126683a --- /dev/null +++ b/templates/partials/tu/proposals.html @@ -0,0 +1,120 @@ +
+

{% trans %}{{ title }}{% endtrans %}

+ + {% if title == "Current Votes" and request.user.has_credential(creds.TU_ADD_VOTE) %} + + {% endif %} + + {% if not results %} +

+ {% trans %}No results found.{% endtrans %} +

+ {% else %} + + + + + + + + + {% if title != "Current Votes" %} + + + {% endif %} + + + + + + {% for result in results %} + + + + + {% set submitted = result.Submitted | dt | as_timezone(timezone) %} + + + + {% set end = result.End | dt | as_timezone(timezone) %} + + + + + {% if title != "Current Votes" %} + + + {% endif %} + + {% set vote = (result | get_vote(request)) %} + + + {% endfor %} + +
{{ "Proposal" | tr }} + {% set off_qs = "%s=%d" | format(off_param, off) %} + {% set by_qs = "%s=%s" | format(by_param, by_next | quote_plus) %} + + {{ "Start" | tr }} + + {{ "End" | tr }}{{ "User" | tr }}{{ "Yes" | tr }}{{ "No" | tr }}{{ "Voted" | tr }}
+ + {% set agenda = result.Agenda[:prev_len] %} + {{ agenda }} + {{ submitted.strftime("%Y-%m-%d") }}{{ end.strftime("%Y-%m-%d") }} + {% if not result.User %} + N/A + {% else %} + + {{ result.User }} + + {% endif %} + {{ result.Yes }}{{ result.No }} + {% if vote %} + + {{ "Yes" | tr }} + + {% else %} + + {{ "No" | tr }} + + {% endif %} +
+ +
+

+ {% if total_votes > pp %} + + {% if off > 0 %} + {% set off_qs = "%s=%d" | format(off_param, off - 10) %} + {% set by_qs = "%s=%s" | format(by_param, by | quote_plus) %} + + ‹ Back + + {% endif %} + + {% if off < total_votes - pp %} + {% set off_qs = "%s=%d" | format(off_param, off + 10) %} + {% set by_qs = "%s=%s" | format(by_param, by | quote_plus) %} + + Next › + + {% endif %} + + {% endif %} +

+
+ + {% endif %} + +
diff --git a/templates/passreset.html b/templates/passreset.html new file mode 100644 index 00000000..d2c3c2ee --- /dev/null +++ b/templates/passreset.html @@ -0,0 +1,76 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{% trans %}Password Reset{% endtrans %}

+ +

+ {% if step == "confirm" %} + {% trans %}Check your e-mail for the confirmation link.{% endtrans %} + {% elif step == "complete" %} + {% trans %}Your password has been reset successfully.{% endtrans %} + {% elif resetkey %} + + {% include "partials/error.html" %} + +

+ + + + + + + + + + + + + + + +
{% trans %}Confirm your user name or primary e-mail address:{% endtrans %} + +
{% trans %}Enter your new password:{% endtrans %} + +
{% trans %}Confirm your new password:{% endtrans %} + +
+
+ + +
+ {% else %} + + {% set url = "https://mailman.archlinux.org/mailman/listinfo/aur-general" %} + {{ "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." + | tr + | format( + '' | format(url), + "") + | safe + }} +

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

+ {% trans %}Enter your user name or your primary e-mail address:{% endtrans %} + +

+ +
+ {% endif %} +

+
+{% endblock %} diff --git a/templates/pkgbase/comaintainers.html b/templates/pkgbase/comaintainers.html new file mode 100644 index 00000000..06e8b9d7 --- /dev/null +++ b/templates/pkgbase/comaintainers.html @@ -0,0 +1,40 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% if errors %} +
    + {% for error in errors %} +
  • {{ error | tr }}
  • + {% endfor %} +
+ {% endif %} + +
+

{{ "Manage Co-maintainers" | tr }}:

+

+ {{ + "Use this form to add co-maintainers for %s%s%s " + "(one user name per line):" + | tr | format("", pkgbase.Name, "") + | safe + }} +

+ +
+
+

+ + +

+ +

+ +

+
+
+ +
+{% endblock %} diff --git a/templates/pkgbase/comments/edit.html b/templates/pkgbase/comments/edit.html new file mode 100644 index 00000000..f938287e --- /dev/null +++ b/templates/pkgbase/comments/edit.html @@ -0,0 +1,44 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{{ "Edit comment for: %s" | tr | format(comment.PackageBase.Name) }}

+ +
+
+
+ +
+ +

+ {{ + "Git commit identifiers referencing commits in " + "the AUR package repository and URLs are converted " + "to links automatically." | tr + }} + {{ + "%sMarkdown syntax%s is partiaully supported." + | tr | format( + '', + "" + ) | safe + }} +

+ +

+ +

+ +

+ +

+ +
+
+ +
+{% endblock %} diff --git a/templates/pkgbase/delete.html b/templates/pkgbase/delete.html new file mode 100644 index 00000000..55eaeba2 --- /dev/null +++ b/templates/pkgbase/delete.html @@ -0,0 +1,74 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + + {% if errors %} +
    + {% for error in errors %} +
  • {{ error | tr }}
  • + {% endfor %} +
+ {% endif %} + +
+

{{ "Delete Package" | tr }}: {{ pkgbase.Name }}

+ +

+ {{ + "Use this form to delete the package base %s%s%s and " + "the following packages from the AUR: " + | tr | format("", pkgbase.Name, "") | safe + }} +

+ +
    + {% for package in pkgbase.packages.all() %} +
  • {{ package.Name }}
  • + {% 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 + }} +

+ +

+ {{ + "Deletion of a package is permanent. " + "Select the checkbox to confirm action." | tr + }} +

+ +
+
+ +

+ + +

+ +

+ +

+ +

+ +

+
+
+ +
+{% endblock %} diff --git a/templates/pkgbase/disown.html b/templates/pkgbase/disown.html new file mode 100644 index 00000000..0ceecfa1 --- /dev/null +++ b/templates/pkgbase/disown.html @@ -0,0 +1,73 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + + {% if errors %} +
    + {% for error in errors %} +
  • {{ error | tr }}
  • + {% endfor %} +
+ {% endif %} + +
+

{{ "Disown Package" | tr }}: {{ pkgbase.Name }}

+ +

+ {{ + "Use this form to disown the package base %s%s%s which " + "includes the following packages: " + | tr | format("", pkgbase.Name, "") | safe + }} +

+ +
    + {% for package in pkgbase.packages.all() %} +
  • {{ package.Name }}
  • + {% 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 + }} +

+ +

+ {{ + "By selecting the checkbox, you confirm that you want to " + "disown the package." | tr + }} +

+ +
+
+

+ + +

+ +

+ +

+

+ +

+
+
+ +
+{% endblock %} diff --git a/templates/pkgbase/flag-comment.html b/templates/pkgbase/flag-comment.html new file mode 100644 index 00000000..4df6ee4b --- /dev/null +++ b/templates/pkgbase/flag-comment.html @@ -0,0 +1,46 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{{ "Flagged Out-of-Date Comment: %s" | tr | format(pkgbase.Name) }}

+ + {# Prepare wrapping for the username. #} + {% set wrap = ["", ""] %} + {% if request.user.is_authenticated() %} + {# When logged in, we wrap it with a link to the account. #} + {% set wrap = ['' | format(pkgbase.Flagger.Username), ""] %} + {% endif %} + + {# Prepare OutOfDateTS as a datetime object in the request user's timezone. #} + {% set flagged_at = pkgbase.OutOfDateTS | dt | as_timezone(timezone) %} + {% set username = "%s%s%s" | format(wrap[0], pkgbase.Flagger.Username, wrap[1]) %} + +

+ {{ + "%s%s%s flagged %s%s%s out-of-date on %s%s%s for the " + "following reason:" + | tr | format("", username, "", + "", pkgbase.Name, "", + "", flagged_at.strftime("%Y-%m-%d"), "") + | safe + }} +

+ + {# Padding #} +

+ +
+
+

{{ pkgbase.FlaggerComment }}

+
+
+ +
+ +
+ + {# Padding #} +

+ +
+{% endblock %} diff --git a/templates/pkgbase/flag.html b/templates/pkgbase/flag.html new file mode 100644 index 00000000..0335cf18 --- /dev/null +++ b/templates/pkgbase/flag.html @@ -0,0 +1,71 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{{ "Flag Package Out-Of-Date" | tr }}: {{ pkgbase.Name }}

+ +

+ {{ + "Use this form to flag the package base %s%s%s and " + "the following packages out-of-date: " + | tr | format("", pkgbase.Name, "") | safe + }} +

+ +
    + {% for package in pkgbase.packages.all() %} +
  • {{ package.Name }}
  • + {% endfor %} +
+ + {% if pkgbase.Name.endswith(('-cvs', '-svn', '-git', '-hg', '-bzr', '-darcs')) %} +

+ {# TODO: This error is not yet translated. #} + {{ + "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." | tr | format("", "") | safe + }} +

+ {% endif %} + +

+ {{ + "Please do %snot%s use this form to report bugs. " + "Use the package comments instead." + | tr | format("", "") | safe + }} + {{ + "Enter details on why the package is out-of-date below, " + "preferably including links to the release announcement " + "or the new release tarball." | tr + }} +

+ + {% if errors %} +
    + {% for error in errors %} +
  • {{ error | tr }}
  • + {% endfor %} +
+ {% endif %} + +
+
+

+ + +

+

+ +

+
+
+
+{% endblock %} diff --git a/templates/pkgbase/index.html b/templates/pkgbase/index.html new file mode 100644 index 00000000..05583494 --- /dev/null +++ b/templates/pkgbase/index.html @@ -0,0 +1,23 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% include "partials/packages/search.html" %} +
+

{{ 'Package Base Details' | tr }}: {{ pkgbase.Name }}

+ + {% set result = pkgbase %} + {% include "partials/packages/actions.html" %} + {% include "partials/packages/details.html" %} + +
+ {% include "partials/packages/pkgbase_metadata.html" %} +
+
+ + + + + {% set pkgname = result.Name %} + {% set pkgbase_id = result.ID %} + {% include "partials/packages/comments.html" %} +{% endblock %} diff --git a/templates/pkgbase/merge.html b/templates/pkgbase/merge.html new file mode 100644 index 00000000..981bd649 --- /dev/null +++ b/templates/pkgbase/merge.html @@ -0,0 +1,93 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + + {% if errors %} +
    + {% for error in errors %} +
  • {{ error | tr }}
  • + {% endfor %} +
+ {% endif %} + +
+

{{ "Merge Package" | tr }}: {{ pkgbase.Name }}

+ +

+ {{ + "Use this form to merge the package base %s%s%s into " + "another package." + | tr | format("", pkgbase.Name, "") | safe + }} + {{ "The following packages will be deleted: " | tr }} +

+ +
    + {% for package in pkgbase.packages.all() %} +
  • {{ package.Name }}
  • + {% 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 + }} +

+ +

+ {{ "Once the package has been merged it cannot be reversed. " | tr }} + {{ "Enter the package name you wish to merge the package into. " | tr }} + {{ "Select the checkbox to confirm action." | tr }} +

+ +
+
+ + +

+ + +

+ +

+ + +

+ +

+ +

+ +

+ +

+
+
+ + {# Bootstrap typeahead-pkgbase-merge.js #} + + +
+ +{% endblock %} diff --git a/templates/pkgbase/request.html b/templates/pkgbase/request.html new file mode 100644 index 00000000..79d47ce4 --- /dev/null +++ b/templates/pkgbase/request.html @@ -0,0 +1,109 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} + {% if errors %} +
    + {% for error in errors %} +
  • {{ error | tr }}
  • + {% endfor %} +
+ {% endif %} + +
+

{{ "Submit Request" | tr }}: {{ pkgbase.Name }}

+ +

+ {{ "Use this form to file a request against package base " + "%s%s%s which includes the following packages:" + | tr | format("", pkgbase.Name, "") | safe }} +

+
    + {% for package in pkgbase.packages %} +
  • {{ package.Name }}
  • + {% endfor %} +
+ + {# Request form #} +
+
+

+ + +

+ + {# Javascript included for HTML-changing triggers depending + on the selected type (above). #} + + + + +

+ + +

+ +

+ {{ + "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 " + "abandoned by upstream, as well as illegal and " + "irreparably broken packages." | tr + }} +

+ + + + + +

+ +

+ +
+
+ +
+{% endblock %} diff --git a/templates/pkgbase/voters.html b/templates/pkgbase/voters.html new file mode 100644 index 00000000..be86f01f --- /dev/null +++ b/templates/pkgbase/voters.html @@ -0,0 +1,27 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

+ {{ "Votes" | tr }} for + + {{ pkgbase.Name }} + +

+ +
+
    + {% for pkg_vote in pkgbase.package_votes %} +
  • + + {{ pkg_vote.User.Username }} + + + {% set voted_at = pkg_vote.VoteTS | dt | as_timezone(timezone) %} + ({{ voted_at.strftime("%Y-%m-%d %H:%M") }}) +
  • + {% endfor %} +
+
+
+{% endblock %} diff --git a/templates/register.html b/templates/register.html new file mode 100644 index 00000000..a15971a1 --- /dev/null +++ b/templates/register.html @@ -0,0 +1,30 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{% trans %}Register{% endtrans %}

+ + {% if complete %} + {{ + "The account, %s%s%s, has been successfully created." + | tr + | format("", "'" + user.Username + "'", "") + | safe + }} +

+ {% trans %}A password reset key has been sent to your e-mail address.{% endtrans %} +

+ {% else %} + {% if errors %} + {% include "partials/error.html" %} + {% else %} +

+ {% trans %}Use this form to create an account.{% endtrans %} +

+ {% endif %} + + {% set form_type = "NewAccount" %} + {% include "partials/account_form.html" %} + {% endif %} +
+{% endblock %} diff --git a/templates/requests.html b/templates/requests.html new file mode 100644 index 00000000..c9e63e60 --- /dev/null +++ b/templates/requests.html @@ -0,0 +1,127 @@ +{% extends "partials/layout.html" %} + +{% set singular = "%d package request found." %} +{% set plural = "%d package requests found." %} + +{% block pageContent %} +
+ {% if not total %} +

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

+ {% else %} + {% include "partials/pager.html" %} + + + + + + + + + + + + + {% for result in results %} + + {% if result.PackageBase %} + {# If the PackageBase still exists, link to it. #} + + {% else %} + {# Otherwise, just display PackageBaseName unlinked. #} + + {% endif %} + {# Type #} + + {# Comments #} + + + {% set idle_time = config_getint("options", "request_idle_time") %} + {% set time_delta = (utcnow - result.RequestTS) | int %} + + {% set due = result.Status == 0 and time_delta > idle_time %} + + + + {% endfor %} + +
{{ "Package" | tr }}{{ "Type" | tr }}{{ "Comments" | tr }}{{ "Filed by" | tr }}{{ "Date" | tr }}{{ "Status" | tr }}
+ + {{ result.PackageBaseName }} + + {{ result.PackageBaseName }} + {{ 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 }}) + {% endif %} + {{ result.Comments }} + {# Filed by #} + + {{ result.User.Username }} + + + {# Date #} + {% set date = result.RequestTS | dt | as_timezone(timezone) %} + {{ date.strftime("%Y-%m-%d %H:%M") }} + + {# Status #} + {% if result.Status == 0 %} + {% set temp_q = { "next": "/requests" } %} + + {% if result.RequestType.ID == 1 %} + {% set action = "delete" %} + {% elif result.RequestType.ID == 2 %} + {% set action = "disown" %} + {% elif result.RequestType.ID == 3 %} + {% set action = "merge" %} + {# Add the 'via' url query parameter. #} + {% set temp_q = temp_q | extend_query( + ["into", result.MergeBaseName] + ) %} + {% endif %} + + {% if request.user.is_elevated() and not result.ClosedTS %} + {# + If RequestType is an orphan and it's not yet due, it's locked + to allow the maintainer time to react to such a request. + + On request, orphans are locked for two weeks. + #} + {% if result.RequestType.ID == 2 and not due %} + {% set time_left = idle_time - time_delta %} + {% if time_left > 48 * 3600 %} + {% set n = round(time_left / (24 * 3600)) %} + {% set time_left_fmt = (n | tn("~%d day left", "~%d days left") | format(n)) %} + {% elif time_left > 3600 %} + {% set n = round(time_left / 3600) %} + {% set time_left_fmt = (n | tn("~%d hour left", "~%d hours left") | format(n)) %} + {% else %} + {% set time_left_fmt = ("<1 hour left" | tr) %} + {% endif %} + {{ "Locked" | tr }} + ({{ time_left_fmt }}) + {% else %} + {# Only elevated users (TU or Dev) are allowed to accept requests. #} + + {{ "Accept" | tr }} + + {% endif %} +
+ {% endif %} + {% if not result.ClosedTS %} + + {{ "Close" | tr }} + + {% endif %} + {% else %} + {{ result.status_display() }} + {% endif %} +
+ {% include "partials/pager.html" %} + {% endif %} +
+{% endblock %} diff --git a/templates/requests/close.html b/templates/requests/close.html new file mode 100644 index 00000000..df767eae --- /dev/null +++ b/templates/requests/close.html @@ -0,0 +1,42 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

{{ "Close Request" | tr }}: {{ pkgreq.PackageBaseName }}

+ +

+ {{ + "Use this form to close the request for package base %s%s%s." + | tr | format("", pkgreq.PackageBaseName, "") + | safe + }} +

+ +

+ {{ "Note" | tr }}: + {{ + "The comments field can be left empty. However, it is highly " + "recommended to add a comment when rejecting a request." + | tr + }} +

+ +
+
+

+ + +

+ +

+ +

+ +
+
+ +
+{% endblock %} diff --git a/templates/testing/PKGBUILD.j2 b/templates/testing/PKGBUILD.j2 new file mode 100644 index 00000000..29d3a1d9 --- /dev/null +++ b/templates/testing/PKGBUILD.j2 @@ -0,0 +1,14 @@ +pkgname={{ pkg.PackageBase.Name }} +pkgver={{ pkg.Version }} +pkgrel=1 +pkgdesc='{{ pkg.Description }}' +url='{{ pkg.URL }}' +arch='any' +license=({{ licenses | join(" ") }}) +depends=({{ depends | join(" ") }}) +source=() +md5sums=() + +package() { + {{ body }} +} diff --git a/templates/testing/SRCINFO.j2 b/templates/testing/SRCINFO.j2 new file mode 100644 index 00000000..873b9c1b --- /dev/null +++ b/templates/testing/SRCINFO.j2 @@ -0,0 +1,10 @@ +pkgbase = {{ pkg.PackageBase.name }} + pkgver = {{ pkg.Version }} + pkgrel = 1 + pkgdesc = {{ pkg.Description }} + url = {{ pkg.URL }} + arch='any' + license = {{ pkg.package_licenses | join(", ", attribute="License.Name") }} + depends = {{ pkg.package_dependencies | join(", ", attribute="DepName") }} + +pkgname = {{ pkg.Name }} diff --git a/templates/testing/alpm_package.j2 b/templates/testing/alpm_package.j2 new file mode 100644 index 00000000..0e741729 --- /dev/null +++ b/templates/testing/alpm_package.j2 @@ -0,0 +1,16 @@ +%FILENAME% +{{ pkgname }}-{{ pkgver }}-{{ arch }}.pkg.tar.xz + +%NAME% +{{ pkgname }} + +%VERSION% +{{ pkgver }}-1 + +%ARCH% +{{ arch }} + +{% if provides %} +%PROVIDES% +{{ provides | join("\n") }} +{% endif %} diff --git a/templates/tos/index.html b/templates/tos/index.html new file mode 100644 index 00000000..a084034e --- /dev/null +++ b/templates/tos/index.html @@ -0,0 +1,46 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+

AUR {% trans %}Terms of Service{% endtrans %}

+
+
+

+ {{ + "Logged-in as: %s" + | tr | format( + "" + request.user.Username + "") + | safe + }} +

+

+ {{ + "The following documents have been updated. " + "Please review them carefully:" | tr + }} +

+
    + {% for term in unaccepted_terms %} +
  • + {{ term.Description }} + (revision {{ term.Revision }}) +
  • + {% endfor %} +
+

+ {% for term in unaccepted_terms %} + + {% endfor %} + + {{ "I accept the terms and conditions above." | tr }} +

+

+ +

+
+
+
+{% endblock %} diff --git a/templates/tu/index.html b/templates/tu/index.html new file mode 100644 index 00000000..5060e1f7 --- /dev/null +++ b/templates/tu/index.html @@ -0,0 +1,35 @@ +{% 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/templates/tu/show.html b/templates/tu/show.html new file mode 100644 index 00000000..c36a3e8f --- /dev/null +++ b/templates/tu/show.html @@ -0,0 +1,21 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
+ {% include "partials/tu/proposal/details.html" %} +
+ + {% if utcnow >= voteinfo.End %} +
+ {% include "partials/tu/proposal/voters.html" %} +
+ {% endif %} + +
+ {% if error %} + {{ error | tr }} + {% else %} + {% include "partials/tu/proposal/form.html" %} + {% endif %} +
+{% endblock %} diff --git a/test/Makefile b/test/Makefile index 758befa3..a6abc9de 100644 --- a/test/Makefile +++ b/test/Makefile @@ -2,17 +2,30 @@ T = $(sort $(wildcard *.t)) PROVE := $(shell command -v prove 2> /dev/null) +MAKEFLAGS = -j1 + +# IMPORTANT: `sh` should come somewhere AFTER `pytest`. +check: sh pytest + +pytest: + cd .. && coverage run --append /usr/bin/pytest test + ifdef PROVE -check: +sh: prove . else -check: $(T) +sh: $(T) endif +coverage: + cd .. && coverage report --include='aurweb/*' + cd .. && coverage xml --include='aurweb/*' + clean: $(RM) -r test-results/ + rm -f ../.coverage $(T): - @echo "*** $@ ***"; $(SHELL) $@ + @echo "*** $@ ***"; $(SHELL) $@ -v -.PHONY: check $(FOREIGN_TARGETS) clean $(T) +.PHONY: check coverage $(FOREIGN_TARGETS) clean $(T) diff --git a/test/README.md b/test/README.md index de7eff18..0d7a85e2 100644 --- a/test/README.md +++ b/test/README.md @@ -1,37 +1,185 @@ +aurweb Test Collection +====================== + +To run all tests, you may run `make -C test sh` and `pytest` within +the project root: + + $ make -C test sh # Run Sharness tests. + $ poetry run pytest # Run Pytest suites. + +For more control, you may use the `prove` or `pytest` command, which receives a +directory or a list of files to run, and produces a report. + +Each test script is standalone, so you may run them individually. Some tests +may receive command-line options to help debugging. + +Dependencies +------------ + +For all tests to run dependencies provided via `poetry` are required: + + $ poetry install + +Logging +------- + +Tests also require the `logging.test.conf` logging configuration +file to be used. You can specify the `LOG_CONFIG` environment +variable to override: + + $ export LOG_CONFIG=logging.test.conf + +`logging.test.conf` enables debug logging for the aurweb package, +for which we run tests against. + +Test Configuration +------------------ + +To perform any tests, we need to supply `aurweb` with a valid +configuration. For development (and testing) purposes, an example +[conf/config.dev](../conf/config.dev) can be slightly modified. + +Start off by copying `config.dev` to a new configuration. + + $ cp -v conf/config.dev conf/config + +First, we must tell `aurweb` where the root of our project +lives by replacing `YOUR_AUR_ROOT` with the path to the aurweb +repository. + + $ sed -i "s;YOUR_AUR_ROOT;/path/to/aurweb;g" conf/config + +Test Databases +-------------- + +Python tests create and drop hashed database names based on +`PYTEST_CURRENT_TEST`. To run tests with a database, the database +user must have privileges to create and drop their own databases. +Typically, this is the root user, but can be configured for any +other user: + + GRANT ALL ON *.* TO 'user'@'localhost' WITH GRANT OPTION + +The aurweb platform is intended to use the `mysql` backend, but +the `sqlite` backend is still used for sharness tests. These tests +will soon be replaced with pytest suites and `sqlite` removed. + +After ensuring you've configured a test database, users can continue +on to [Running Tests](#running-tests). + Running tests ------------- -To run all the tests, you may run `make check` under `test/`. +Makefile test targets: `sh`, `clean`. -For more control, you may use the `prove` command, which receives a directory -or a list of files to run, and produces a report. +Recommended method of running tests: `pytest`. -Each test script is standalone, so you may run them individually. Some tests -may receive command-line options to help debugging. See for example sharness's -documentation for shell test scripts: -https://github.com/chriscool/sharness/blob/master/README.git +Legacy sharness tests: `make -C test sh`. -### Dependencies +aurweb is currently going through a refactor where the majority of +`sharness` tests have been replaced with `pytest` units. There are +still a few `sharness` tests around, and they are required to gain +as much coverage as possible over an entire test run. Users should +be writing `pytest` units for any new features. -For all the test to run, the following Arch packages should be installed: +Run tests from the project root. -- pyalpm -- python-alembic -- python-bleach -- python-markdown -- python-pygit2 -- python-sqlalchemy -- python-srcinfo + $ cd /path/to/aurweb -Writing tests -------------- +Ensure you have the proper `AUR_CONFIG` and `LOG_CONFIG` exported: -Test scripts must follow the Test Anything Protocol specification: + $ export AUR_CONFIG=conf/config + $ export LOG_CONFIG=logging.test.conf + +To run `sharness` shell test suites (requires Arch Linux): + + $ make -C test sh + +To run `pytest` Python test suites: + + # With poetry-installed aurweb + $ poetry run pytest + + # With globally-installed aurweb + $ pytest + +After tests are run, one can produce coverage reports. + + # Print out a CLI coverage report. + $ coverage report + + # Produce an HTML-based coverage report. + $ coverage html + +Writing Python tests (current) +------------------------------ + +Almost all of our `pytest` suites use the database in some way. There +are a few particular testing utilities in `aurweb` that one should +keep aware of to aid testing code: + +- `db_test` pytest fixture + - Prepares test databases for the module and cleans out database + tables for each test function requiring this fixture. +- `aurweb.testing.requests.Request` + - A fake stripped down version of `fastapi.Request` that can + be passed to any functions in our codebase which use + `fastapi.Request` parameters. + +Example code: + + import pytest + + from aurweb import db + from aurweb.models.account_type import USER_ID + from aurweb.models.user import User + from aurweb.testing.requests import Request + + # We need to use the `db_test` fixture at some point + # during our test functions. + @pytest.fixture(autouse=True) + def setup(db_test: None) -> None: + return + + # Or... specify it in a dependency fixture. + @pytest.fixture + def user(db_test: None) -> User: + with db.begin(): + user = db.create(User, Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID) + yield user + + def test_user_login(user: User): + assert isinstance(user, User) is True + + fake_request = Request() + sid = user.login(fake_request, "testPassword") + assert sid is not None + +Writing Sharness tests (legacy) +------------------------------- + +Shell test scripts must follow the Test Anything Protocol specification: http://testanything.org/tap-specification.html +Python tests must be compatible with `pytest` and included in `pytest test/` +execution after setting up a configuration. + Tests must support being run from any directory. They may use $0 to determine their location. Python scripts should expect aurweb to be installed and importable without toying with os.path or PYTHONPATH. Tests written in shell should use sharness. In general, new tests should be consistent with existing tests unless they have a good reason not to. + +Debugging Sharness tests +--------------- + +By default, `make -C test` is quiet and does not print out verbose information +about tests being run. If a test is failing, one can look into verbose details +of sharness tests by executing them with the `--verbose` flag. Example: +`./t1100_git_auth.t --verbose`. This is particularly useful when tests happen +to fail in a remote continuous integration environment, where the reader does +not have complete access to the runner. diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..283c979a --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,231 @@ +""" +pytest configuration. + +The conftest.py file is used to define pytest-global fixtures +or actions run before tests. + +Module scoped fixtures: +---------------------- +- setup_database +- db_session (depends: setup_database) + +Function scoped fixtures: +------------------------ +- db_test (depends: db_session) + +Tests in aurweb which access the database **must** use the `db_test` +function fixture. Most database tests simply require this fixture in +an autouse=True setup fixture, or for fixtures used in DB tests example: + + # In scenarios which there are no other database fixtures + # or other database fixtures dependency paths don't always + # hit `db_test`. + @pytest.fixture(autouse=True) + def setup(db_test): + return + + # In scenarios where we can embed the `db_test` fixture in + # specific fixtures that already exist. + @pytest.fixture + def user(db_test): + with db.begin(): + user = db.create(User, ...) + yield user + +The `db_test` fixture triggers our module-level database fixtures, +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 sqlalchemy import create_engine +from sqlalchemy.engine import URL +from sqlalchemy.engine.base import Engine +from sqlalchemy.exc import ProgrammingError +from sqlalchemy.orm import scoped_session + +import aurweb.config +import aurweb.db + +from aurweb import initdb, logging, testing +from aurweb.testing.email import Email +from aurweb.testing.filelock import FileLock +from aurweb.testing.git import GitRepository + +logger = logging.get_logger(__name__) + +# Synchronization lock for database setup. +setup_lock = Lock() + + +def test_engine() -> Engine: + """ + Return a privileged SQLAlchemy engine with no database. + + This method is particularly useful for providing an engine that + can be used to create and drop databases from an SQL server. + + :return: SQLAlchemy Engine instance (not connected to a database) + """ + 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), + "host": aurweb.config.get("database", "host"), + "port": aurweb.config.get_with_fallback("database", "port", None), + "query": { + "unix_socket": unix_socket + } + } + + backend = aurweb.config.get("database", "backend") + driver = aurweb.db.DRIVERS.get(backend) + return create_engine(URL.create(driver, **kwargs)) + + +class AlembicArgs: + """ + Masquerade an ArgumentParser like structure. + + This structure is needed to pass conftest-specific arguments + to initdb.run duration database creation. + """ + verbose = False + use_alembic = True + + +def _create_database(engine: Engine, dbname: str) -> None: + """ + Create a test database. + + :param engine: Engine returned by test_engine() + :param dbname: Database name to create + """ + conn = engine.connect() + try: + conn.execute(f"CREATE DATABASE {dbname}") + except ProgrammingError: # pragma: no cover + # The database most likely already existed if we hit + # a ProgrammingError. Just drop the database and try + # again. If at that point things still fail, any + # exception will be propogated up to the caller. + conn.execute(f"DROP DATABASE {dbname}") + conn.execute(f"CREATE DATABASE {dbname}") + conn.close() + initdb.run(AlembicArgs) + + +def _drop_database(engine: Engine, dbname: str) -> None: + """ + Drop a test database. + + :param engine: Engine returned by test_engine() + :param dbname: Database name to drop + """ + aurweb.schema.metadata.drop_all(bind=engine) + conn = engine.connect() + conn.execute(f"DROP DATABASE {dbname}") + conn.close() + + +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) + + # 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. """ + 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) + yield # Run the test function depending on this fixture. + _drop_database(engine, dbname) # Cleanup the database. + + +@pytest.fixture(scope="module") +def db_session(setup_database: None) -> scoped_session: + """ + Yield a database session based on aurweb.db.name(). + + The returned session is popped out of persistence after the test is run. + """ + # After the test runs, aurweb.db.name() ends up returning the + # configured database, because PYTEST_CURRENT_TEST is removed. + dbname = aurweb.db.name() + session = aurweb.db.get_session() + + yield session + + # Close the session and pop it. + session.close() + aurweb.db.pop_session(dbname) + + +@pytest.fixture +def db_test(db_session: scoped_session) -> None: + """ + Database test fixture. + + This fixture should be included in any tests which access the + database. It ensures that a test database is created and + alembic migrated, takes care of dropping the database when + the module is complete, and runs setup_test_db() to clear out + tables for each test. + + Tests using this fixture should access the database + session via aurweb.db.get_session(). + """ + testing.setup_test_db() + + +@pytest.fixture +def git(tmpdir: py.path.local) -> GitRepository: + yield GitRepository(tmpdir) + + +@pytest.fixture +def email_test() -> None: + """ + A decoupled test email setup fixture. + + When using the `db_test` fixture, this fixture is redundant. Otherwise, + email tests need to run through our `setup_email` function to ensure + that we set them up to be used via aurweb.testing.email.Email. + """ + setup_email() diff --git a/test/scripts/cover b/test/scripts/cover new file mode 100755 index 00000000..1ebe596d --- /dev/null +++ b/test/scripts/cover @@ -0,0 +1,13 @@ +#!/bin/sh +# This script is used by sharness tests hosted in our `test` +# directory. We require a concrete script to make using this easily, +# because we often call `env` in those tests. +# +# The purpose of this script is to allow sharness tests to gather +# Python coverage when calling scripts within `aurweb`. +# +TOPLEVEL=$(dirname "$0")/../.. + +# Define a COVERAGE_FILE in our root directory. +COVERAGE_FILE="$TOPLEVEL/.coverage" \ +coverage run -L --source="$TOPLEVEL/aurweb" --append "$@" diff --git a/test/setup.sh b/test/setup.sh index 285401b0..232c33b7 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -18,6 +18,11 @@ AURBLUP="$TOPLEVEL/aurweb/scripts/aurblup.py" NOTIFY="$TOPLEVEL/aurweb/scripts/notify.py" RENDERCOMMENT="$TOPLEVEL/aurweb/scripts/rendercomment.py" +# We reuse some of these scripts when running `env`, so add +# it to PATH; that way, env can pick up the script when loaded. +PATH="${PATH}:${TOPLEVEL}/test/scripts" +export PATH + # Create the configuration file and a dummy notification script. cat >config <<-EOF [database] @@ -25,6 +30,7 @@ backend = sqlite name = aur.db [options] +aurwebdir = $TOPLEVEL aur_location = https://aur.archlinux.org aur_request_ml = aur-requests@lists.archlinux.org enable-maintenance = 0 @@ -61,6 +67,7 @@ sync-dbs = test server = file://$(pwd)/remote/ [mkpkglists] +archivedir = $(pwd)/archive packagesfile = packages.gz packagesmetafile = packages-meta-v1.json.gz packagesmetaextfile = packages-meta-ext-v1.json.gz @@ -144,6 +151,7 @@ export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME mkdir aur.git cd aur.git git init -q + git config --local commit.gpgsign false git checkout -q --orphan refs/namespaces/foobar/refs/heads/master diff --git a/test/t1100-git-auth.t b/test/t1100-git-auth.t index cbf16aed..e29bccdd 100755 --- a/test/t1100-git-auth.t +++ b/test/t1100-git-auth.t @@ -4,24 +4,25 @@ test_description='git-auth tests' . "$(dirname "$0")/setup.sh" + test_expect_success 'Test basic authentication.' ' - "$GIT_AUTH" "$AUTH_KEYTYPE_USER" "$AUTH_KEYTEXT_USER" >out && + cover "$GIT_AUTH" "$AUTH_KEYTYPE_USER" "$AUTH_KEYTEXT_USER" >out && grep -q AUR_USER=user out && grep -q AUR_PRIVILEGED=0 out ' test_expect_success 'Test Trusted User authentication.' ' - "$GIT_AUTH" "$AUTH_KEYTYPE_TU" "$AUTH_KEYTEXT_TU" >out && + cover "$GIT_AUTH" "$AUTH_KEYTYPE_TU" "$AUTH_KEYTEXT_TU" >out && grep -q AUR_USER=tu out && grep -q AUR_PRIVILEGED=1 out ' test_expect_success 'Test authentication with an unsupported key type.' ' - test_must_fail "$GIT_AUTH" ssh-xxx "$AUTH_KEYTEXT_USER" + test_must_fail cover "$GIT_AUTH" ssh-xxx "$AUTH_KEYTEXT_USER" ' test_expect_success 'Test authentication with a wrong key.' ' - "$GIT_AUTH" "$AUTH_KEYTYPE_MISSING" "$AUTH_KEYTEXT_MISSING" >out + cover "$GIT_AUTH" "$AUTH_KEYTYPE_MISSING" "$AUTH_KEYTEXT_MISSING" >out test_must_be_empty out ' diff --git a/test/t1200-git-serve.t b/test/t1200-git-serve.t index 1893cdcd..f1657412 100755 --- a/test/t1200-git-serve.t +++ b/test/t1200-git-serve.t @@ -2,14 +2,14 @@ test_description='git-serve tests' -. "$(dirname "$0")/setup.sh" +. "$(dirname $0)/setup.sh" test_expect_success 'Test interactive shell.' ' - "$GIT_SERVE" 2>&1 | grep -q "Interactive shell is disabled." + cover "$GIT_SERVE" 2>&1 | grep -q "Interactive shell is disabled." ' test_expect_success 'Test help.' ' - SSH_ORIGINAL_COMMAND=help "$GIT_SERVE" 2>actual && + SSH_ORIGINAL_COMMAND=help cover "$GIT_SERVE" 2>actual && save_IFS=$IFS IFS= while read -r line; do @@ -25,7 +25,7 @@ test_expect_success 'Test maintenance mode.' ' sed "s/^\(enable-maintenance = \)0$/\\11/" config.old >config && test_must_fail \ env SSH_ORIGINAL_COMMAND=help \ - "$GIT_SERVE" 2>actual && + cover "$GIT_SERVE" 2>actual && cat >expected <<-EOF && The AUR is down due to maintenance. We will be back soon. EOF @@ -34,7 +34,7 @@ test_expect_success 'Test maintenance mode.' ' ' test_expect_success 'Test IP address logging.' ' - SSH_ORIGINAL_COMMAND=help AUR_USER=user "$GIT_SERVE" 2>actual && + SSH_ORIGINAL_COMMAND=help AUR_USER=user cover "$GIT_SERVE" 2>actual && cat >expected <<-EOF && 1.2.3.4 EOF @@ -48,7 +48,7 @@ test_expect_success 'Test IP address bans.' ' SSH_CLIENT="1.3.3.7 1337 22" && test_must_fail \ env SSH_ORIGINAL_COMMAND=help \ - "$GIT_SERVE" 2>actual && + cover "$GIT_SERVE" 2>actual && cat >expected <<-EOF && The SSH interface is disabled for your IP address. EOF @@ -58,14 +58,14 @@ test_expect_success 'Test IP address bans.' ' test_expect_success 'Test setup-repo and list-repos.' ' SSH_ORIGINAL_COMMAND="setup-repo foobar" AUR_USER=user \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="setup-repo foobar2" AUR_USER=tu \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && *foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -77,7 +77,7 @@ test_expect_success 'Test git-receive-pack.' ' EOF SSH_ORIGINAL_COMMAND="git-receive-pack /foobar.git/" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -85,7 +85,7 @@ test_expect_success 'Test git-receive-pack with an invalid repository name.' ' test_must_fail \ env SSH_ORIGINAL_COMMAND="git-receive-pack /!.git/" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual + cover "$GIT_SERVE" 2>&1 >actual ' test_expect_success "Test git-upload-pack." ' @@ -96,7 +96,7 @@ test_expect_success "Test git-upload-pack." ' EOF SSH_ORIGINAL_COMMAND="git-upload-pack /foobar.git/" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -108,7 +108,7 @@ test_expect_success "Try to pull from someone else's repository." ' EOF SSH_ORIGINAL_COMMAND="git-upload-pack /foobar2.git/" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -116,7 +116,7 @@ test_expect_success "Try to push to someone else's repository." ' test_must_fail \ env SSH_ORIGINAL_COMMAND="git-receive-pack /foobar2.git/" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 + cover "$GIT_SERVE" 2>&1 ' test_expect_success "Try to push to someone else's repository as Trusted User." ' @@ -127,7 +127,7 @@ test_expect_success "Try to push to someone else's repository as Trusted User." EOF SSH_ORIGINAL_COMMAND="git-receive-pack /foobar.git/" \ AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -139,40 +139,40 @@ test_expect_success "Test restore." ' foobar EOF SSH_ORIGINAL_COMMAND="restore foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual + cover "$GIT_SERVE" 2>&1 >actual test_cmp expected actual ' test_expect_success "Try to restore an existing package base." ' test_must_fail \ - env SSH_ORIGINAL_COMMAND="restore foobar2" \ + env SSH_ORIGINAL_COMMAND="restore foobar2"\ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 + cover "$GIT_SERVE" 2>&1 ' test_expect_success "Disown all package bases." ' SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' test_expect_success "Adopt a package base as a regular user." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && *foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -180,119 +180,119 @@ test_expect_success "Adopt an already adopted package base." ' test_must_fail \ env SSH_ORIGINAL_COMMAND="adopt foobar" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 + 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 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && *foobar2 EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' test_expect_success "Disown one's own package base as a regular user." ' SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && 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 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 >actual && + 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 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && test_must_fail \ env SSH_ORIGINAL_COMMAND="adopt foobar2" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && cat >expected <<-EOF && *foobar2 EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 + cover "$GIT_SERVE" 2>&1 ' test_expect_success "Try to steal another user's package as a Trusted User." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && cat >expected <<-EOF && *foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&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 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && test_must_fail \ env SSH_ORIGINAL_COMMAND="disown foobar2" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && *foobar2 EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 + cover "$GIT_SERVE" 2>&1 ' test_expect_success "Try to disown another user's package as a Trusted User." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ - "$GIT_SERVE" 2>&1 + cover "$GIT_SERVE" 2>&1 ' test_expect_success "Adopt a package base and add co-maintainers." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="set-comaintainers foobar user3 user4" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 5|3|1 6|3|2 @@ -305,7 +305,7 @@ test_expect_success "Adopt a package base and add co-maintainers." ' test_expect_success "Update package base co-maintainers." ' SSH_ORIGINAL_COMMAND="set-comaintainers foobar user2 user3 user4" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 4|3|1 5|3|2 @@ -320,7 +320,7 @@ test_expect_success "Try to add co-maintainers to an orphan package base." ' test_must_fail \ env SSH_ORIGINAL_COMMAND="set-comaintainers foobar2 user2 user3 user4" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 4|3|1 5|3|2 @@ -333,12 +333,12 @@ test_expect_success "Try to add co-maintainers to an orphan package base." ' test_expect_success "Disown a package base and check (co-)maintainer list." ' SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && *foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user2 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && cat >expected <<-EOF && 5|3|1 @@ -351,11 +351,11 @@ 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 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user3 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 >actual && + cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && cat >expected <<-EOF && EOF @@ -366,7 +366,7 @@ test_expect_success "Force-disown a package base and check (co-)maintainer list. test_expect_success "Check whether package requests are closed when disowning." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + 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, "", ""); @@ -374,7 +374,7 @@ test_expect_success "Check whether package requests are closed when disowning." EOD >sendmail.out && SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat <<-EOD >expected && Subject: [PRQ#1] Orphan Request for foobar Accepted EOD @@ -389,7 +389,7 @@ test_expect_success "Check whether package requests are closed when disowning." test_expect_success "Flag a package base out-of-date." ' SSH_ORIGINAL_COMMAND="flag foobar Because." AUR_USER=user2 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 1|Because. EOF @@ -400,7 +400,7 @@ test_expect_success "Flag a package base out-of-date." ' test_expect_success "Unflag a package base as flagger." ' SSH_ORIGINAL_COMMAND="unflag foobar" AUR_USER=user2 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 0|Because. EOF @@ -411,11 +411,11 @@ test_expect_success "Unflag a package base as flagger." ' test_expect_success "Unflag a package base as maintainer." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="flag foobar Because." AUR_USER=user2 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="unflag foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 0|Because. EOF @@ -426,9 +426,9 @@ test_expect_success "Unflag a package base as maintainer." ' test_expect_success "Unflag a package base as random user." ' SSH_ORIGINAL_COMMAND="flag foobar Because." AUR_USER=user2 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && SSH_ORIGINAL_COMMAND="unflag foobar" AUR_USER=user3 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 1|Because. EOF @@ -439,11 +439,11 @@ test_expect_success "Unflag a package base as random user." ' test_expect_success "Flag using a comment which is too short." ' SSH_ORIGINAL_COMMAND="unflag foobar" AUR_USER=user2 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && test_must_fail \ env SSH_ORIGINAL_COMMAND="flag foobar xx" \ AUR_USER=user2 AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 0|Because. EOF @@ -454,7 +454,7 @@ test_expect_success "Flag using a comment which is too short." ' test_expect_success "Vote for a package base." ' SSH_ORIGINAL_COMMAND="vote foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 3|1 EOF @@ -472,7 +472,7 @@ test_expect_success "Vote for a package base." ' test_expect_success "Vote for a package base twice." ' test_must_fail \ env SSH_ORIGINAL_COMMAND="vote foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && 3|1 EOF @@ -489,7 +489,7 @@ test_expect_success "Vote for a package base twice." ' test_expect_success "Remove vote from a package base." ' SSH_ORIGINAL_COMMAND="unvote foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF echo "SELECT PackageBaseID, UsersID FROM PackageVotes;" | \ @@ -507,7 +507,7 @@ test_expect_success "Try to remove the vote again." ' test_must_fail \ env SSH_ORIGINAL_COMMAND="unvote foobar" \ AUR_USER=user AUR_PRIVILEGED=0 \ - "$GIT_SERVE" 2>&1 && + cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF echo "SELECT PackageBaseID, UsersID FROM PackageVotes;" | \ diff --git a/test/t1300-git-update.t b/test/t1300-git-update.t index 82c0fb99..e9d943c0 100755 --- a/test/t1300-git-update.t +++ b/test/t1300-git-update.t @@ -16,7 +16,7 @@ test_expect_success 'Test update hook on a fresh repository.' ' old=0000000000000000000000000000000000000000 && new=$(git -C aur.git rev-parse HEAD^) && AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && cat >expected <<-EOF && 1|1|foobar|1-1|aurweb test package.|https://aur.archlinux.org/ 1|GPL @@ -34,7 +34,7 @@ test_expect_success 'Test update hook on another fresh repository.' ' git -C aur.git checkout -q refs/namespaces/foobar2/refs/heads/master && new=$(git -C aur.git rev-parse HEAD) && AUR_USER=user AUR_PKGBASE=foobar2 AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && cat >expected <<-EOF && 1|1|foobar|1-1|aurweb test package.|https://aur.archlinux.org/ 2|2|foobar2|1-1|aurweb test package.|https://aur.archlinux.org/ @@ -55,7 +55,7 @@ test_expect_success 'Test update hook on an updated repository.' ' old=$(git -C aur.git rev-parse HEAD^) && new=$(git -C aur.git rev-parse HEAD) && AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && cat >expected <<-EOF && 2|2|foobar2|1-1|aurweb test package.|https://aur.archlinux.org/ 3|1|foobar|1-2|aurweb test package.|https://aur.archlinux.org/ @@ -74,7 +74,7 @@ test_expect_success 'Test update hook on an updated repository.' ' test_expect_success 'Test restore mode.' ' AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" restore 2>&1 && + cover "$GIT_UPDATE" restore 2>&1 && cat >expected <<-EOF && 2|2|foobar2|1-1|aurweb test package.|https://aur.archlinux.org/ 3|1|foobar|1-2|aurweb test package.|https://aur.archlinux.org/ @@ -97,7 +97,7 @@ test_expect_success 'Test restore mode on a non-existent repository.' ' EOD test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar3 AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" restore >actual 2>&1 && + cover "$GIT_UPDATE" restore >actual 2>&1 && test_cmp expected actual ' @@ -109,7 +109,7 @@ test_expect_success 'Pushing to a branch other than master.' ' EOD test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/pu "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/pu "$old" "$new" >actual 2>&1 && test_cmp expected actual ' @@ -121,7 +121,7 @@ test_expect_success 'Performing a non-fast-forward ref update.' ' EOD test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' @@ -133,7 +133,7 @@ test_expect_success 'Performing a non-fast-forward ref update as Trusted User.' EOD test_must_fail \ env AUR_USER=tu AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && test_cmp expected actual ' @@ -145,7 +145,7 @@ test_expect_success 'Performing a non-fast-forward ref update as normal user wit EOD test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 AUR_OVERWRITE=1 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && test_cmp expected actual ' @@ -153,7 +153,7 @@ test_expect_success 'Performing a non-fast-forward ref update as Trusted User wi 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 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 ' test_expect_success 'Removing .SRCINFO.' ' @@ -164,7 +164,7 @@ test_expect_success 'Removing .SRCINFO.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing .SRCINFO$" actual ' @@ -177,7 +177,7 @@ test_expect_success 'Removing .SRCINFO with a follow-up fix.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing .SRCINFO$" actual ' @@ -189,7 +189,7 @@ test_expect_success 'Removing PKGBUILD.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing PKGBUILD$" actual ' @@ -203,7 +203,7 @@ test_expect_success 'Pushing a tree with a subdirectory.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: the repository must not contain subdirectories$" actual ' @@ -216,7 +216,7 @@ test_expect_success 'Pushing a tree with a large blob.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: maximum blob size (250.00KiB) exceeded$" actual ' @@ -232,7 +232,7 @@ test_expect_success 'Pushing .SRCINFO with a non-matching package base.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: invalid pkgbase: foobar2, expected foobar$" actual ' @@ -248,7 +248,7 @@ test_expect_success 'Pushing .SRCINFO with invalid syntax.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 ' test_expect_success 'Pushing .SRCINFO without pkgver.' ' @@ -263,7 +263,7 @@ test_expect_success 'Pushing .SRCINFO without pkgver.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing mandatory field: pkgver$" actual ' @@ -279,7 +279,7 @@ test_expect_success 'Pushing .SRCINFO without pkgrel.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing mandatory field: pkgrel$" actual ' @@ -294,7 +294,7 @@ test_expect_success 'Pushing .SRCINFO with epoch.' ' ) && new=$(git -C aur.git rev-parse HEAD) && AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && cat >expected <<-EOF && 2|2|foobar2|1-1|aurweb test package.|https://aur.archlinux.org/ 3|1|foobar|1:1-2|aurweb test package.|https://aur.archlinux.org/ @@ -315,7 +315,7 @@ test_expect_success 'Pushing .SRCINFO with invalid pkgname.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: invalid package name: !$" actual ' @@ -331,7 +331,7 @@ test_expect_success 'Pushing .SRCINFO with invalid epoch.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: invalid epoch: !$" actual ' @@ -348,7 +348,7 @@ test_expect_success 'Pushing .SRCINFO with too long URL.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: url field too long: $url\$" actual ' @@ -364,7 +364,7 @@ test_expect_success 'Missing install file.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing install file: install$" actual ' @@ -380,7 +380,7 @@ test_expect_success 'Missing changelog file.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing changelog file: changelog$" actual ' @@ -396,7 +396,7 @@ test_expect_success 'Missing source file.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: missing source file: file$" actual ' @@ -413,7 +413,7 @@ test_expect_success 'Pushing .SRCINFO with too long source URL.' ' new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && grep -q "^error: source entry too long: $url\$" actual ' @@ -428,7 +428,7 @@ test_expect_success 'Pushing a blacklisted package.' ' EOD test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' @@ -442,7 +442,7 @@ test_expect_success 'Pushing a blacklisted package as Trusted User.' ' warning: package is blacklisted: forbidden EOD AUR_USER=tu AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' @@ -457,7 +457,7 @@ test_expect_success 'Pushing a package already in the official repositories.' ' EOD test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' @@ -471,7 +471,7 @@ test_expect_success 'Pushing a package already in the official repositories as T warning: package already provided by [core]: official EOD AUR_USER=tu AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' @@ -491,7 +491,7 @@ test_expect_success 'Trying to hijack a package.' ' EOD test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar2 AUR_PRIVILEGED=0 \ - "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' diff --git a/test/t2100-mkpkglists.t b/test/t2100-mkpkglists.t deleted file mode 100755 index 5bf13de8..00000000 --- a/test/t2100-mkpkglists.t +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/sh - -test_description='mkpkglists tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test package list generation with no packages.' ' - echo "DELETE FROM Packages;" | sqlite3 aur.db && - echo "DELETE FROM PackageBases;" | sqlite3 aur.db && - "$MKPKGLISTS" && - test $(zcat packages.gz | wc -l) -eq 1 && - test $(zcat pkgbase.gz | wc -l) -eq 1 -' - -test_expect_success 'Test package list generation.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1, "foobar", 1, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (2, "foobar2", 2, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (3, "foobar3", NULL, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (4, "foobar4", 1, 0, 0, ""); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (1, 1, "pkg1"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (2, 1, "pkg2"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (3, 1, "pkg3"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (4, 2, "pkg4"); - INSERT INTO Packages (ID, PackageBaseID, Name) VALUES (5, 3, "pkg5"); - EOD - "$MKPKGLISTS" && - cat <<-EOD >expected && - foobar - foobar2 - foobar4 - EOD - gunzip pkgbase.gz && - sed "/^#/d" pkgbase >actual && - test_cmp actual expected && - cat <<-EOD >expected && - pkg1 - pkg2 - pkg3 - pkg4 - EOD - gunzip packages.gz && - sed "/^#/d" packages >actual && - test_cmp actual expected -' - -test_expect_success 'Test user list generation.' ' - "$MKPKGLISTS" && - cat <<-EOD >expected && - dev - tu - tu2 - tu3 - tu4 - user - user2 - user3 - user4 - EOD - gunzip users.gz && - sed "/^#/d" users >actual && - test_cmp actual expected -' - -test_done diff --git a/test/t2200-tuvotereminder.t b/test/t2200-tuvotereminder.t deleted file mode 100755 index 5a8f3a25..00000000 --- a/test/t2200-tuvotereminder.t +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -test_description='tuvotereminder tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test Trusted User vote reminders.' ' - now=$(date -d now +%s) && - tomorrow=$(date -d tomorrow +%s) && - threedays=$(date -d "3 days" +%s) && - cat <<-EOD | sqlite3 aur.db && - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (1, "Lorem ipsum.", "user", 0, $now, 0.00, 2); - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (2, "Lorem ipsum.", "user", 0, $tomorrow, 0.00, 2); - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (3, "Lorem ipsum.", "user", 0, $tomorrow, 0.00, 2); - INSERT INTO TU_VoteInfo (ID, Agenda, User, Submitted, End, Quorum, SubmitterID) VALUES (4, "Lorem ipsum.", "user", 0, $threedays, 0.00, 2); - EOD - >sendmail.out && - "$TUVOTEREMINDER" && - grep -q "Proposal 2" sendmail.out && - grep -q "Proposal 3" sendmail.out && - test_must_fail grep -q "Proposal 1" sendmail.out && - test_must_fail grep -q "Proposal 4" sendmail.out -' - -test_expect_success 'Check that only TUs who did not vote receive reminders.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO TU_Votes (VoteID, UserID) VALUES (1, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (2, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (3, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (4, 2); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (1, 7); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (3, 7); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (2, 8); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (4, 8); - INSERT INTO TU_Votes (VoteID, UserID) VALUES (1, 9); - EOD - >sendmail.out && - "$TUVOTEREMINDER" && - cat <<-EOD >expected && - Subject: TU Vote Reminder: Proposal 2 - To: tu2@localhost - Subject: TU Vote Reminder: Proposal 2 - To: tu4@localhost - Subject: TU Vote Reminder: Proposal 3 - To: tu3@localhost - Subject: TU Vote Reminder: Proposal 3 - To: tu4@localhost - EOD - grep "^\(Subject\|To\)" sendmail.out >sendmail.parts && - test_cmp sendmail.parts expected -' - -test_done diff --git a/test/t2300-pkgmaint.t b/test/t2300-pkgmaint.t deleted file mode 100755 index c390f5db..00000000 --- a/test/t2300-pkgmaint.t +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh - -test_description='pkgmaint tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test package base cleanup script.' ' - now=$(date -d now +%s) && - threedaysago=$(date -d "3 days ago" +%s) && - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1, "foobar", 1, $now, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (2, "foobar2", 2, $threedaysago, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (3, "foobar3", NULL, $now, 0, ""); - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (4, "foobar4", NULL, $threedaysago, 0, ""); - EOD - "$PKGMAINT" && - cat <<-EOD >expected && - foobar - foobar2 - foobar3 - EOD - echo "SELECT Name FROM PackageBases;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_done diff --git a/test/t2400-aurblup.t b/test/t2400-aurblup.t deleted file mode 100755 index cc287a0f..00000000 --- a/test/t2400-aurblup.t +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/sh - -test_description='aurblup tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test official provider update script.' ' - mkdir -p remote/test/foobar-1.0-1 && - cat <<-EOD >remote/test/foobar-1.0-1/desc && - %FILENAME% - foobar-1.0-any.pkg.tar.xz - - %NAME% - foobar - - %VERSION% - 1.0-1 - - %ARCH% - any - EOD - mkdir -p remote/test/foobar2-1.0-1 && - cat <<-EOD >remote/test/foobar2-1.0-1/desc && - %FILENAME% - foobar2-1.0-any.pkg.tar.xz - - %NAME% - foobar2 - - %VERSION% - 1.0-1 - - %ARCH% - any - - %PROVIDES% - foobar3 - foobar4 - EOD - ( cd remote/test && bsdtar -czf ../test.db * ) && - mkdir sync && - "$AURBLUP" && - cat <<-EOD >expected && - foobar|test|foobar - foobar2|test|foobar2 - foobar2|test|foobar3 - foobar2|test|foobar4 - EOD - echo "SELECT Name, Repo, Provides FROM OfficialProviders ORDER BY Provides;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_done diff --git a/test/t2500-notify.t b/test/t2500-notify.t deleted file mode 100755 index c0592c91..00000000 --- a/test/t2500-notify.t +++ /dev/null @@ -1,431 +0,0 @@ -#!/bin/sh - -test_description='notify tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test out-of-date notifications.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package base IDs which can be distinguished from user IDs. */ - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1001, "foobar", 1, 0, 0, "This is a test OOD comment."); - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1002, "foobar2", 2, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1003, "foobar3", NULL, 0, 0, ""); - INSERT INTO PackageBases (ID, Name, MaintainerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1004, "foobar4", 1, 0, 0, ""); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1001, 2, 1); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1001, 4, 2); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1002, 3, 1); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1002, 5, 2); - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1003, 4, 1); - EOD - >sendmail.out && - "$NOTIFY" flag 1 1001 && - cat <<-EOD >expected && - Subject: AUR Out-of-date Notification for foobar - To: tu@localhost - Subject: AUR Out-of-date Notification for foobar - To: user2@localhost - Subject: AUR Out-of-date Notification for foobar - To: user@localhost - EOD - grep "^\(Subject\|To\)" sendmail.out >sendmail.parts && - test_cmp sendmail.parts expected && - cat <<-EOD | sqlite3 aur.db - DELETE FROM PackageComaintainers; - EOD -' - -test_expect_success 'Test subject and body of reset key notifications.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET ResetKey = "12345678901234567890123456789012" WHERE ID = 1; - EOD - >sendmail.out && - "$NOTIFY" send-resetkey 1 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Password Reset - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - 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. - - [1] https://aur.archlinux.org/passreset/?resetkey=12345678901234567890123456789012 - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of welcome notifications.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET ResetKey = "12345678901234567890123456789012" WHERE ID = 1; - EOD - >sendmail.out && - "$NOTIFY" welcome 1 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: Welcome to the Arch User Repository - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - 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. - - [1] https://aur.archlinux.org/passreset/?resetkey=12345678901234567890123456789012 - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of comment notifications.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package comments IDs which can be distinguished from other IDs. */ - INSERT INTO PackageComments (ID, PackageBaseID, UsersID, Comments, RenderedComment) VALUES (2001, 1001, 1, "This is a test comment.", "This is a test comment."); - INSERT INTO PackageNotifications (PackageBaseID, UserID) VALUES (1001, 2); - UPDATE Users SET CommentNotify = 1 WHERE ID = 2; - EOD - >sendmail.out && - "$NOTIFY" comment 1 1001 2001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Comment for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] added the following comment to foobar [2]: - - This is a test comment. - - -- - If you no longer wish to receive notifications about this package, - please go to the package page [2] and select "Disable notifications". - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of update notifications.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET UpdateNotify = 1 WHERE ID = 2; - EOD - >sendmail.out && - "$NOTIFY" update 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Package Update: foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] pushed a new commit to foobar [2]. - - -- - If you no longer wish to receive notifications about this package, - please go to the package page [2] and select "Disable notifications". - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of out-of-date notifications.' ' - >sendmail.out && - "$NOTIFY" flag 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Out-of-date Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Your package foobar [1] has been flagged out-of-date by user [2]: - - This is a test OOD comment. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - [2] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of adopt notifications.' ' - >sendmail.out && - "$NOTIFY" adopt 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Ownership Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - The package foobar [1] was adopted by user [2]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - [2] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of disown notifications.' ' - >sendmail.out && - "$NOTIFY" disown 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Ownership Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - The package foobar [1] was disowned by user [2]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - [2] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of co-maintainer addition notifications.' ' - >sendmail.out && - "$NOTIFY" comaintainer-add 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Co-Maintainer Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - You were added to the co-maintainer list of foobar [1]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of co-maintainer removal notifications.' ' - >sendmail.out && - "$NOTIFY" comaintainer-remove 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Co-Maintainer Notification for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - You were removed from the co-maintainer list of foobar [1]. - - [1] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of delete notifications.' ' - >sendmail.out && - "$NOTIFY" delete 1 1001 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Package deleted: foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] deleted foobar [2]. - - You will no longer receive notifications about this package. - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of merge notifications.' ' - >sendmail.out && - "$NOTIFY" delete 1 1001 1002 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: AUR Package deleted: foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] merged foobar [2] into foobar2 [3]. - - -- - If you no longer wish receive notifications about the new package, - please go to [3] and click "Disable notifications". - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - [3] https://aur.archlinux.org/pkgbase/foobar2/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test Cc, subject and body of request open notifications.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package request IDs which can be distinguished from other IDs. */ - INSERT INTO PackageRequests (ID, PackageBaseID, PackageBaseName, UsersID, ReqTypeID, Comments, ClosureComment) VALUES (3001, 1001, "foobar", 2, 1, "This is a request test comment.", ""); - EOD - >sendmail.out && - "$NOTIFY" request-open 1 3001 orphan 1001 && - grep ^Cc: sendmail.out >actual && - cat <<-EOD >expected && - Cc: user@localhost, tu@localhost - EOD - test_cmp actual expected && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Orphan Request for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] filed an orphan request for foobar [2]: - - This is a request test comment. - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of request open notifications for merge requests.' ' - >sendmail.out && - "$NOTIFY" request-open 1 3001 merge 1001 foobar2 && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Merge Request for foobar - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - user [1] filed a request to merge foobar [2] into foobar2 [3]: - - This is a request test comment. - - [1] https://aur.archlinux.org/account/user/ - [2] https://aur.archlinux.org/pkgbase/foobar/ - [3] https://aur.archlinux.org/pkgbase/foobar2/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test Cc, subject and body of request close notifications.' ' - >sendmail.out && - "$NOTIFY" request-close 1 3001 accepted && - grep ^Cc: sendmail.out >actual && - cat <<-EOD >expected && - Cc: user@localhost, tu@localhost - EOD - test_cmp actual expected && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Deletion Request for foobar Accepted - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Request #3001 has been accepted by user [1]. - - [1] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of request close notifications (auto-accept).' ' - >sendmail.out && - "$NOTIFY" request-close 0 3001 accepted && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Deletion Request for foobar Accepted - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Request #3001 has been accepted automatically by the Arch User - Repository package request system. - EOD - test_cmp actual expected -' - -test_expect_success 'Test Cc of request close notification with co-maintainer.' ' - cat <<-EOD | sqlite3 aur.db && - /* Use package base IDs which can be distinguished from user IDs. */ - INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (1001, 3, 1); - EOD - >sendmail.out && - "$NOTIFY" request-close 0 3001 accepted && - grep ^Cc: sendmail.out >actual && - cat <<-EOD >expected && - Cc: user@localhost, tu@localhost, dev@localhost - EOD - test_cmp actual expected && - cat <<-EOD | sqlite3 aur.db - DELETE FROM PackageComaintainers; - EOD -' - -test_expect_success 'Test subject and body of request close notifications with closure comment.' ' - cat <<-EOD | sqlite3 aur.db && - UPDATE PackageRequests SET ClosureComment = "This is a test closure comment." WHERE ID = 3001; - EOD - >sendmail.out && - "$NOTIFY" request-close 1 3001 accepted && - grep ^Subject: sendmail.out >actual && - cat <<-EOD >expected && - Subject: [PRQ#3001] Deletion Request for foobar Accepted - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Request #3001 has been accepted by user [1]: - - This is a test closure comment. - - [1] https://aur.archlinux.org/account/user/ - EOD - test_cmp actual expected -' - -test_expect_success 'Test subject and body of TU vote reminders.' ' - >sendmail.out && - "$NOTIFY" tu-vote-reminder 1 && - grep ^Subject: sendmail.out | head -1 >actual && - cat <<-EOD >expected && - Subject: TU Vote Reminder: Proposal 1 - EOD - test_cmp actual expected && - sed -n "/^\$/,\$p" sendmail.out | head -4 | base64 -d >actual && - echo >>actual && - cat <<-EOD >expected && - Please remember to cast your vote on proposal 1 [1]. The voting period - ends in less than 48 hours. - - [1] https://aur.archlinux.org/tu/?id=1 - EOD - test_cmp actual expected -' - -test_done diff --git a/test/t2600-rendercomment.t b/test/t2600-rendercomment.t deleted file mode 100755 index e01904c6..00000000 --- a/test/t2600-rendercomment.t +++ /dev/null @@ -1,160 +0,0 @@ -#!/bin/sh - -test_description='rendercomment tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test comment rendering.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageBases (ID, Name, PackagerUID, SubmittedTS, ModifiedTS, FlaggerComment) VALUES (1, "foobar", 1, 0, 0, ""); - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (1, 1, "Hello world! - This is a comment.", ""); - EOD - "$RENDERCOMMENT" 1 && - cat <<-EOD >expected && -

Hello world! - This is a comment.

- EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 1; - EOD - test_cmp actual expected -' - -test_expect_success 'Test Markdown conversion.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (2, 1, "*Hello* [world](https://www.archlinux.org/)!", ""); - EOD - "$RENDERCOMMENT" 2 && - cat <<-EOD >expected && -

Hello world!

- EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 2; - EOD - test_cmp actual expected -' - -test_expect_success 'Test HTML sanitizing.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (3, 1, "", ""); - EOD - "$RENDERCOMMENT" 3 && - cat <<-EOD >expected && - <script>alert("XSS!");</script> - EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 3; - EOD - test_cmp actual expected -' - -test_expect_success 'Test link conversion.' ' - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (4, 1, " - Visit https://www.archlinux.org/#_test_. - Visit *https://www.archlinux.org/*. - Visit . - Visit \`https://www.archlinux.org/\`. - Visit [Arch Linux](https://www.archlinux.org/). - Visit [Arch Linux][arch]. - [arch]: https://www.archlinux.org/ - ", ""); - EOD - "$RENDERCOMMENT" 4 && - cat <<-EOD >expected && -

Visit https://www.archlinux.org/#_test_. - Visit https://www.archlinux.org/. - Visit https://www.archlinux.org/. - Visit https://www.archlinux.org/. - Visit Arch Linux. - Visit Arch Linux.

- EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 4; - EOD - test_cmp actual expected -' - -test_expect_success 'Test Git commit linkification.' ' - local oid=`git -C aur.git rev-parse --verify HEAD` - cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (5, 1, " - $oid - ${oid:0:7} - x.$oid.x - ${oid}x - 0123456789abcdef - \`$oid\` - http://example.com/$oid - ", ""); - EOD - "$RENDERCOMMENT" 5 && - cat <<-EOD >expected && -

${oid:0:12} - ${oid:0:7} - x.${oid:0:12}.x - ${oid}x - 0123456789abcdef - $oid - http://example.com/$oid

- EOD - cat <<-EOD | sqlite3 aur.db >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 5; - EOD - test_cmp actual expected -' - -test_expect_success 'Test Flyspray issue linkification.' ' - sqlite3 aur.db <<-EOD && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (6, 1, " - FS#1234567. - *FS#1234* - FS# - XFS#1 - \`FS#1234\` - https://archlinux.org/?test=FS#1234 - ", ""); - EOD - "$RENDERCOMMENT" 6 && - cat <<-EOD >expected && -

FS#1234567. - FS#1234 - FS# - XFS#1 - FS#1234 - https://archlinux.org/?test=FS#1234

- EOD - sqlite3 aur.db <<-EOD >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 6; - EOD - test_cmp actual expected -' - -test_expect_success 'Test headings lowering.' ' - sqlite3 aur.db <<-EOD && - INSERT INTO PackageComments (ID, PackageBaseID, Comments, RenderedComment) VALUES (7, 1, " - # One - ## Two - ### Three - #### Four - ##### Five - ###### Six - ", ""); - EOD - "$RENDERCOMMENT" 7 && - cat <<-EOD >expected && -
One
-
Two
-
Three
-
Four
-
Five
-
Six
- EOD - sqlite3 aur.db <<-EOD >actual && - SELECT RenderedComment FROM PackageComments WHERE ID = 7; - EOD - test_cmp actual expected -' - -test_done diff --git a/test/t2700-usermaint.t b/test/t2700-usermaint.t deleted file mode 100755 index f0bb449b..00000000 --- a/test/t2700-usermaint.t +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/sh - -test_description='usermaint tests' - -. "$(dirname "$0")/setup.sh" - -test_expect_success 'Test removal of login IP addresses.' ' - now=$(date -d now +%s) && - threedaysago=$(date -d "3 days ago" +%s) && - tendaysago=$(date -d "10 days ago" +%s) && - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET LastLogin = $threedaysago, LastLoginIPAddress = "1.2.3.4" WHERE ID = 1; - UPDATE Users SET LastLogin = $tendaysago, LastLoginIPAddress = "2.3.4.5" WHERE ID = 2; - UPDATE Users SET LastLogin = $now, LastLoginIPAddress = "3.4.5.6" WHERE ID = 3; - UPDATE Users SET LastLogin = 0, LastLoginIPAddress = "4.5.6.7" WHERE ID = 4; - UPDATE Users SET LastLogin = 0, LastLoginIPAddress = "5.6.7.8" WHERE ID = 5; - UPDATE Users SET LastLogin = $tendaysago, LastLoginIPAddress = "6.7.8.9" WHERE ID = 6; - EOD - "$USERMAINT" && - cat <<-EOD >expected && - 1.2.3.4 - 3.4.5.6 - EOD - echo "SELECT LastLoginIPAddress FROM Users WHERE LastLoginIPAddress IS NOT NULL;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_expect_success 'Test removal of SSH login IP addresses.' ' - now=$(date -d now +%s) && - threedaysago=$(date -d "3 days ago" +%s) && - tendaysago=$(date -d "10 days ago" +%s) && - cat <<-EOD | sqlite3 aur.db && - UPDATE Users SET LastSSHLogin = $now, LastSSHLoginIPAddress = "1.2.3.4" WHERE ID = 1; - UPDATE Users SET LastSSHLogin = $threedaysago, LastSSHLoginIPAddress = "2.3.4.5" WHERE ID = 2; - UPDATE Users SET LastSSHLogin = $tendaysago, LastSSHLoginIPAddress = "3.4.5.6" WHERE ID = 3; - UPDATE Users SET LastSSHLogin = 0, LastSSHLoginIPAddress = "4.5.6.7" WHERE ID = 4; - UPDATE Users SET LastSSHLogin = 0, LastSSHLoginIPAddress = "5.6.7.8" WHERE ID = 5; - UPDATE Users SET LastSSHLogin = $tendaysago, LastSSHLoginIPAddress = "6.7.8.9" WHERE ID = 6; - EOD - "$USERMAINT" && - cat <<-EOD >expected && - 1.2.3.4 - 2.3.4.5 - EOD - echo "SELECT LastSSHLoginIPAddress FROM Users WHERE LastSSHLoginIPAddress IS NOT NULL;" | sqlite3 aur.db >actual && - test_cmp actual expected -' - -test_done diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py new file mode 100644 index 00000000..2af7127b --- /dev/null +++ b/test/test_accepted_term.py @@ -0,0 +1,55 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.accepted_term import AcceptedTerm +from aurweb.models.account_type import USER_ID +from aurweb.models.term import Term +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=USER_ID) + yield user + + +@pytest.fixture +def term() -> Term: + with db.begin(): + term = db.create(Term, Description="Test term", + URL="https://test.term") + yield term + + +@pytest.fixture +def accepted_term(user: User, term: Term) -> AcceptedTerm: + with db.begin(): + accepted_term = db.create(AcceptedTerm, User=user, Term=term) + yield accepted_term + + +def test_accepted_term(user: User, term: Term, accepted_term: AcceptedTerm): + # Make sure our AcceptedTerm relationships got initialized properly. + assert accepted_term.User == user + assert accepted_term in user.accepted_terms + assert accepted_term in term.accepted_terms + + +def test_accepted_term_null_user_raises_exception(term: Term): + with pytest.raises(IntegrityError): + AcceptedTerm(Term=term) + + +def test_accepted_term_null_term_raises_exception(user: User): + with pytest.raises(IntegrityError): + AcceptedTerm(User=user) diff --git a/test/test_account_type.py b/test/test_account_type.py new file mode 100644 index 00000000..1d71f878 --- /dev/null +++ b/test/test_account_type.py @@ -0,0 +1,51 @@ +import pytest + +from aurweb import db +from aurweb.models.account_type import AccountType +from aurweb.models.user import User + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def account_type() -> AccountType: + with db.begin(): + account_type_ = db.create(AccountType, AccountType="TestUser") + + yield account_type_ + + with db.begin(): + db.delete(account_type_) + + +def test_account_type(account_type): + """ 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) + + 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) + + assert user.AccountType == account_type + + # This must be db.deleted here to avoid foreign key issues when + # deleting the temporary AccountType in the fixture. + with db.begin(): + db.delete(user) diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py new file mode 100644 index 00000000..01e61101 --- /dev/null +++ b/test/test_accounts_routes.py @@ -0,0 +1,1867 @@ +import re +import tempfile + +from datetime import 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.models.account_type as at + +from aurweb import captcha, db, logging, 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.ban import Ban +from aurweb.models.session import Session +from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint +from aurweb.models.term import Term +from aurweb.models.user import User +from aurweb.testing.html import get_errors +from aurweb.testing.requests import Request + +logger = logging.get_logger(__name__) + +# Some test global constants. +TEST_USERNAME = "test" +TEST_EMAIL = "test@example.org" + + +def make_ssh_pubkey(): + # Create a public key with ssh-keygen (this adds ssh-keygen as a + # 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.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + return open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=app) + + +def create_user(username: str) -> User: + email = f"{username}@example.org" + user = create(User, Username=username, Email=email, + Passwd="testPassword", + AccountTypeID=USER_ID) + return user + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = create_user(TEST_USERNAME) + yield user + + +@pytest.fixture +def tu_user(user: User): + with db.begin(): + user.AccountTypeID = TRUSTED_USER_AND_DEV_ID + yield user + + +def test_get_passreset_authed_redirects(client: TestClient, user: User): + sid = user.login(Request(), "testPassword") + assert sid is not None + + with client as request: + response = request.get("/passreset", cookies={"AURSID": sid}, + allow_redirects=False) + + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + +def test_get_passreset(client: TestClient): + with client as request: + response = request.get("/passreset") + assert response.status_code == int(HTTPStatus.OK) + + +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"}) + + # The header title should be translated. + assert "Passwort zurücksetzen" in response.text + + # The form input label should be translated. + expected = "Benutzername oder primäre E-Mail-Adresse eingeben:" + assert expected in response.text + + # And the button. + assert "Weiter" in response.text + + # Restore english. + with client as request: + response = request.get("/passreset", cookies={"AURLANG": "en"}) + + +def test_get_passreset_with_resetkey(client: TestClient): + with client as request: + response = request.get("/passreset", data={"resetkey": "abcd"}) + assert response.status_code == int(HTTPStatus.OK) + + +def test_post_passreset_authed_redirects(client: TestClient, user: User): + sid = user.login(Request(), "testPassword") + assert sid is not None + + with client as request: + response = request.post("/passreset", + cookies={"AURSID": sid}, + data={"user": "blah"}, + allow_redirects=False) + + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + +def test_post_passreset_user(client: TestClient, user: User): + # With username. + with client as request: + response = request.post("/passreset", data={"user": TEST_USERNAME}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + + # With e-mail. + with client as request: + response = request.post("/passreset", data={"user": TEST_EMAIL}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + + +def test_post_passreset_resetkey(client: TestClient, user: User): + with db.begin(): + user.session = Session(UsersID=user.ID, SessionID="blah", + LastUpdateTS=time.utcnow()) + + # Prepare a password reset. + with client as request: + response = request.post("/passreset", data={"user": TEST_USERNAME}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + + # Now that we've prepared the password reset, prepare a POST + # request with the user's ResetKey. + resetkey = user.ResetKey + post_data = { + "user": TEST_USERNAME, + "resetkey": resetkey, + "password": "abcd1234", + "confirm": "abcd1234" + } + + with client as request: + response = request.post("/passreset", data=post_data) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=complete" + + +def make_resetkey(client: TestClient, user: User): + with client as request: + response = request.post("/passreset", data={"user": TEST_USERNAME}) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/passreset?step=confirm" + return user.ResetKey + + +def make_passreset_data(user: User, resetkey: str): + return { + "user": user.Username, + "resetkey": resetkey + } + + +def test_post_passreset_error_invalid_email(client: TestClient, user: User): + # First, test with a user that doesn't even exist. + with client as request: + response = request.post("/passreset", data={"user": "invalid"}) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + assert "Invalid e-mail." in response.text + + # Then, test with an invalid resetkey for a real user. + _ = make_resetkey(client, user) + post_data = make_passreset_data(user, "fake") + post_data["password"] = "abcd1234" + post_data["confirm"] = "abcd1234" + + with client as request: + response = request.post("/passreset", data=post_data) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + assert "Invalid e-mail." in response.text + + +def test_post_passreset_error_missing_field(client: TestClient, user: User): + # Now that we've prepared the password reset, prepare a POST + # request with the user's ResetKey. + resetkey = make_resetkey(client, user) + post_data = make_passreset_data(user, resetkey) + + with client as request: + response = request.post("/passreset", data=post_data) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + error = "Missing a required field." + assert error in response.content.decode("utf-8") + + +def test_post_passreset_error_password_mismatch(client: TestClient, + user: User): + resetkey = make_resetkey(client, user) + post_data = make_passreset_data(user, resetkey) + + post_data["password"] = "abcd1234" + post_data["confirm"] = "mismatched" + + with client as request: + response = request.post("/passreset", data=post_data) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + error = "Password fields do not match." + assert error in response.content.decode("utf-8") + + +def test_post_passreset_error_password_requirements(client: TestClient, + user: User): + resetkey = make_resetkey(client, user) + post_data = make_passreset_data(user, resetkey) + + passwd_min_len = User.minimum_passwd_length() + assert passwd_min_len >= 4 + + post_data["password"] = "x" + post_data["confirm"] = "x" + + with client as request: + response = request.post("/passreset", data=post_data) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + error = f"Your password must be at least {passwd_min_len} characters." + assert error in response.content.decode("utf-8") + + +def test_get_register(client: TestClient): + with client as request: + response = request.get("/register") + assert response.status_code == int(HTTPStatus.OK) + + +def post_register(request, **kwargs): + """ 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) + + data = { + "U": "newUser", + "E": "newUser@email.org", + "P": "newUserPassword", + "C": "newUserPassword", + "L": "en", + "TZ": "UTC", + "captcha": answer, + "captcha_salt": salt + } + + # For any kwargs given, override their k:v pairs in data. + args = dict(kwargs) + for k, v in args.items(): + data[k] = v + + return request.post("/register", data=data, allow_redirects=False) + + +def test_post_register(client: TestClient): + with client as request: + response = post_register(request) + assert response.status_code == int(HTTPStatus.OK) + + expected = "The account, 'newUser', " + expected += "has been successfully created." + assert expected in response.content.decode() + + +def test_post_register_rejects_case_insensitive_spoof(client: TestClient): + with client as request: + response = post_register(request, U="newUser", E="newUser@example.org") + assert response.status_code == int(HTTPStatus.OK) + + with client as request: + response = post_register(request, U="NEWUSER", E="BLAH@GMAIL.COM") + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + expected = "The username, NEWUSER, is already in use." + assert expected in response.content.decode() + + with client as request: + response = post_register(request, U="BLAH", E="NEWUSER@EXAMPLE.ORG") + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + expected = "The address, NEWUSER@EXAMPLE.ORG, " + expected += "is already in use." + assert expected in response.content.decode() + + +def test_post_register_error_expired_captcha(client: TestClient): + with client as request: + response = post_register(request, captcha_salt="invalid-salt") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "This CAPTCHA has expired. Please try again." in content + + +def test_post_register_error_missing_captcha(client: TestClient): + with client as request: + response = post_register(request, captcha=None) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The CAPTCHA is missing." in content + + +def test_post_register_error_invalid_captcha(client: TestClient): + with client as request: + response = post_register(request, captcha="invalid blah blah") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The entered CAPTCHA answer is invalid." in content + + +def test_post_register_error_ip_banned(client: TestClient): + # 'testclient' is used as request.client.host via FastAPI TestClient. + with db.begin(): + create(Ban, IPAddress="testclient", BanTS=datetime.utcnow()) + + with client as request: + response = post_register(request) + + 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 + + +def test_post_register_error_missing_username(client: TestClient): + with client as request: + response = post_register(request, U="") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Missing a required field." in content + + +def test_post_register_error_missing_email(client: TestClient): + with client as request: + response = post_register(request, E="") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Missing a required field." in content + + +def test_post_register_error_invalid_username(client: TestClient): + with client as request: + # Our test config requires at least three characters for a + # valid username, so test against two characters: 'ba'. + response = post_register(request, U="ba") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The username is invalid." in content + + +def test_post_register_invalid_password(client: TestClient): + with client as request: + response = post_register(request, P="abc", C="abc") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"Your password must be at least \d+ characters." + assert re.search(expected, content) + + +def test_post_register_error_missing_confirm(client: TestClient): + with client as request: + response = post_register(request, C=None) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Please confirm your new password." in content + + +def test_post_register_error_mismatched_confirm(client: TestClient): + with client as request: + response = post_register(request, C="mismatched") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Password fields do not match." in content + + +def test_post_register_error_invalid_email(client: TestClient): + with client as request: + response = post_register(request, E="bad@email") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The email address is invalid." in content + + +def test_post_register_error_undeliverable_email(client: TestClient): + with client as request: + # At the time of writing, webchat.freenode.net does not contain + # mx records; if it ever does, it'll break this test. + response = post_register(request, E="email@bad.c") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The email address is invalid." in content + + +def test_post_register_invalid_backup_email(client: TestClient): + with client as request: + response = post_register(request, BE="bad@email") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The backup email address is invalid." in content + + +def test_post_register_error_invalid_homepage(client: TestClient): + with client as request: + response = post_register(request, HP="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "The home page is invalid, please specify the full HTTP(s) URL." + assert expected in content + + +def test_post_register_error_invalid_pgp_fingerprints(client: TestClient): + with client as request: + response = post_register(request, K="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "The PGP key fingerprint is invalid." + assert expected in content + + pk = 'z' + ('a' * 39) + with client as request: + response = post_register(request, K=pk) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "The PGP key fingerprint is invalid." + assert expected in content + + +def test_post_register_error_invalid_ssh_pubkeys(client: TestClient): + with client as request: + response = post_register(request, PK="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The SSH public key is invalid." in content + + with client as request: + response = post_register(request, PK="ssh-rsa ") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "The SSH public key is invalid." in content + + +def test_post_register_error_unsupported_language(client: TestClient): + with client as request: + response = post_register(request, L="bad") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "Language is not currently supported." + assert expected in content + + +def test_post_register_error_unsupported_timezone(client: TestClient): + with client as request: + response = post_register(request, TZ="ABCDEFGH") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = "Timezone is not currently supported." + assert expected in content + + +def test_post_register_error_username_taken(client: TestClient, user: User): + with client as request: + response = post_register(request, U="test") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"The username, .*, is already in use." + assert re.search(expected, content) + + +def test_post_register_error_email_taken(client: TestClient, user: User): + with client as request: + response = post_register(request, E="test@example.org") + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"The address, .*, is already in use." + assert re.search(expected, content) + + +def test_post_register_error_ssh_pubkey_taken(client: TestClient, user: User): + pk = str() + + # Create a public key with ssh-keygen (this adds ssh-keygen as a + # 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.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + # Take the sha256 fingerprint of the ssh public key, create it. + fp = get_fingerprint(pk) + with db.begin(): + create(SSHPubKey, UserID=user.ID, PubKey=pk, Fingerprint=fp) + + with client as request: + response = post_register(request, PK=pk) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + expected = r"The SSH public key, .*, is already in use." + assert re.search(expected, content) + + +def test_post_register_with_ssh_pubkey(client: TestClient): + pk = str() + + # Create a public key with ssh-keygen (this adds ssh-keygen as a + # 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.wait() + assert proc.returncode == 0 + + # Read in the public key, then delete the temp dir we made. + pk = open(f"{tmpdir}/test.ssh.pub").read().rstrip() + + with client as request: + response = post_register(request, PK=pk) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_get_account_edit_tu_as_tu(client: TestClient, tu_user: User): + """ Test edit get route of another TU as a TU. """ + with db.begin(): + user2 = create_user("test2") + user2.AccountTypeID = at.TRUSTED_USER_ID + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + + with client as request: + response = request.get(endpoint, cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Verify that we have an account type selection and that the + # "{at.TRUSTED_USER}" option is selected. + root = parse_root(response.text) + atype = root.xpath('//select[@id="id_type"]/option[@selected="selected"]') + expected = at.TRUSTED_USER + assert atype[0].text.strip() == expected + + username = root.xpath('//input[@id="id_username"]')[0] + assert username.attrib["value"] == user2.Username + email = root.xpath('//input[@id="id_email"]')[0] + 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. """ + with db.begin(): + user2 = create_user("test2") + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + + with client as request: + response = request.get(endpoint, cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Verify that we have an account type selection and that the + # "Normal {at.USER}" option is selected. + root = parse_root(response.text) + atype = root.xpath('//select[@id="id_type"]/option[@selected="selected"]') + expected = f"Normal {at.USER}" + assert atype[0].text.strip() == expected + + # Other fields should be available and match up. + username = root.xpath('//input[@id="id_username"]')[0] + assert username.attrib["value"] == user2.Username + email = root.xpath('//input[@id="id_email"]')[0] + assert email.attrib["value"] == user2.Email + + +def test_get_account_edit_type(client: TestClient, user: User): + """ 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) + 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): + with db.begin(): + user2 = create_user("test_tu") + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + + with client as request: + response = request.get(endpoint, cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + atype = root.xpath('//select[@id="id_type"]/option[@selected="selected"]') + assert atype[0].text.strip() == f"Normal {at.USER}" + + +def test_get_account_edit_unauthorized(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + with db.begin(): + 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) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + expected = f"/account/{user2.Username}" + assert response.headers.get("location") == expected + + +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" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + expected = "The account, test, " + expected += "has been successfully modified." + assert expected in response.content.decode() + + +def test_post_account_edit_type_as_tu(client: TestClient, tu_user: User): + with db.begin(): + user2 = create_user("test_tu") + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + data = { + "U": user2.Username, + "E": user2.Email, + "T": at.USER_ID, + "passwd": "testPassword", + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + +def test_post_account_edit_type_as_dev(client: TestClient, tu_user: User): + with db.begin(): + user2 = create_user("test2") + tu_user.AccountTypeID = at.DEVELOPER_ID + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + data = { + "U": user2.Username, + "E": user2.Email, + "T": at.DEVELOPER_ID, + "passwd": "testPassword", + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + 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): + with db.begin(): + user2 = create_user("test_tu") + tu_user.AccountTypeID = at.TRUSTED_USER_ID + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + data = { + "U": user2.Username, + "E": user2.Email, + "T": at.DEVELOPER_ID, + "passwd": "testPassword", + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + 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}.") + 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() + with db.begin(): + user.AccountType = tu_or_dev + + request = Request() + sid = tu_user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test666@example.org", + "passwd": "testPassword" + } + + endpoint = f"/account/{tu_user.Username}/edit" + with client as request: + response = request.post(endpoint, cookies={"AURSID": sid}, + data=post_data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + expected = "The account, test, " + expected += "has been successfully modified." + assert expected in response.content.decode() + + +def test_post_account_edit_language(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "L": "de", # German + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + # Parse the response content html into an lxml root, then make + # sure we see a 'de' option selected on the page. + content = response.content.decode() + root = lxml.html.fromstring(content) + lang_nodes = root.xpath('//option[@value="de"]/@selected') + assert lang_nodes and len(lang_nodes) != 0 + assert lang_nodes[0] == "selected" + + +def test_post_account_edit_timezone(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "TZ": "CET", + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + +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": "" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Invalid password." in content + + +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" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + content = response.content.decode() + assert "Invalid password." in content + + +def test_post_account_edit_inactivity_unauthorized(client: TestClient, + user: User): + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_data = { + "U": "test", + "E": "test@example.org", + "J": True, + "passwd": "testPassword" + } + with client as request: + resp = request.post(f"/account/{user.Username}/edit", data=post_data, + cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + errors = get_errors(resp.text) + expected = "You do not have permission to suspend accounts." + assert errors[0].text.strip() == expected + + +def test_post_account_edit_inactivity(client: TestClient, user: User): + with db.begin(): + user.AccountTypeID = TRUSTED_USER_ID + assert not user.Suspended + + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_data = { + "U": "test", + "E": "test@example.org", + "J": True, + "passwd": "testPassword" + } + with client as request: + resp = request.post(f"/account/{user.Username}/edit", data=post_data, + cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # Make sure the user record got updated correctly. + assert user.InactivityTS > 0 + + post_data.update({"J": False}) + with client as request: + resp = request.post(f"/account/{user.Username}/edit", data=post_data, + cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + assert user.InactivityTS == 0 + + +def test_post_account_edit_suspended(client: TestClient, user: User): + with db.begin(): + user.AccountTypeID = TRUSTED_USER_ID + assert not user.Suspended + + cookies = {"AURSID": user.login(Request(), "testPassword")} + post_data = { + "U": "test", + "E": "test@example.org", + "S": True, + "passwd": "testPassword" + } + endpoint = f"/account/{user.Username}/edit" + with client as request: + resp = request.post(endpoint, data=post_data, cookies=cookies) + 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 + + +def test_post_account_edit_error_unauthorized(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + with db.begin(): + 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" + } + + 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) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + expected = f"/account/{user2.Username}" + assert response.headers.get("location") == expected + + +def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "PK": make_ssh_pubkey(), + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + # Now let's update what's already there to gain coverage over that path. + 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) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": user.Username, + "E": user.Email, + "PK": make_ssh_pubkey(), + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + post_data = { + "U": user.Username, + "E": user.Email, + "PK": str(), # Pass an empty string now to walk the delete path. + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_post_account_edit_invalid_ssh_pubkey(client: TestClient, user: User): + pubkey = "ssh-rsa fake key" + + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "P": "newPassword", + "C": "newPassword", + "PK": pubkey, + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + +def test_post_account_edit_password(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + post_data = { + "U": "test", + "E": "test@example.org", + "P": "newPassword", + "C": "newPassword", + "passwd": "testPassword" + } + + with client as request: + response = request.post("/account/test/edit", cookies={ + "AURSID": sid + }, data=post_data, allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + assert user.valid_password("newPassword") + + +def test_post_account_edit_self_type_as_user(client: TestClient, user: User): + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{user.Username}/edit" + + with client as request: + resp = request.get(endpoint, cookies=cookies) + 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" + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + errors = get_errors(resp.text) + expected = "You do not have permission to change account types." + assert errors[0].text.strip() == expected + + +def test_post_account_edit_other_user_as_user(client: TestClient, user: User): + with db.begin(): + user2 = create_user("test2") + + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + + with client as request: + resp = request.get(endpoint, cookies=cookies, + allow_redirects=False) + 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" + + # 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) + 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, + "T": USER_ID, + "passwd": "testPassword" + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + assert tu_user.AccountTypeID == USER_ID + + +def test_post_account_edit_other_user_type_as_tu( + client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture): + caplog.set_level(DEBUG) + + with db.begin(): + user2 = create_user("test2") + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/edit" + + # As a TU, we can see the Account Type field for other users. + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + assert "id_type" in resp.text + + # As a TU, we can modify other user's account types. + data = { + "U": user2.Username, + "E": user2.Email, + "T": TRUSTED_USER_ID, + "passwd": "testPassword" + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # Let's make sure the DB got updated properly. + assert user2.AccountTypeID == TRUSTED_USER_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}.") + 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): + with db.begin(): + user2 = create_user("test2") + + cookies = {"AURSID": tu_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" + } + with client as request: + resp = request.post(endpoint, data=data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + errors = get_errors(resp.text) + expected = "Invalid account type provided." + assert errors[0].text.strip() == expected + + +def test_get_account(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + with client as request: + response = request.get("/account/test", cookies={"AURSID": sid}, + allow_redirects=False) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_get_account_not_found(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + with client as request: + response = request.get("/account/not_found", cookies={"AURSID": sid}, + allow_redirects=False) + + 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) + 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. """ + sid = user.login(Request(), "testPassword") + cookies = {"AURSID": sid} + + with client as request: + response = request.get("/accounts", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + parser = lxml.etree.HTMLParser() + root = lxml.etree.fromstring(response.text, parser=parser) + + # Get the form. + form = root.xpath('//form[contains(@class, "account-search-form")]') + + # Make sure there's only one form and it goes where it should. + assert len(form) == 1 + form = next(iter(form)) + assert form.attrib.get("method") == "post" + assert form.attrib.get("action") == "/accounts" + + def field(element): + """ 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"]')) + assert bool(username) + + account_type = form.xpath(field('select[@id="id_type"]')) + assert bool(account_type) + + suspended = form.xpath(field('input[@id="id_suspended"]')) + assert bool(suspended) + + email = form.xpath(field('input[@id="id_email"]')) + assert bool(email) + + realname = form.xpath(field('input[@id="id_realname"]')) + assert bool(realname) + + irc = form.xpath(field('input[@id="id_irc"]')) + assert bool(irc) + + sortby = form.xpath(field('select[@id="id_sortby"]')) + assert bool(sortby) + + +def parse_root(html): + parser = lxml.etree.HTMLParser() + return lxml.etree.fromstring(html, parser=parser) + + +def get_rows(html): + root = parse_root(html) + return root.xpath('//table[contains(@class, "users")]/tbody/tr') + + +def test_post_accounts(client: TestClient, user: User, tu_user: User): + # Set a PGPKey. + with db.begin(): + user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" + + # Create a few more users. + users = [user] + with db.begin(): + for i in range(10): + _user = create_user(f"test_{i}") + users.append(_user) + + sid = user.login(Request(), "testPassword") + cookies = {"AURSID": sid} + + with client as request: + response = request.post("/accounts", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 11 + + # Simulate default ascending ORDER_BY. + sorted_users = sorted(users, key=lambda u: u.Username) + for i, _user in enumerate(sorted_users): + columns = rows[i].xpath("./td") + assert len(columns) == 7 + + username, atype, suspended, real_name, \ + irc_nick, pgp_key, edit = columns + + username = next(iter(username.xpath("./a"))) + assert username.text.strip() == _user.Username + + assert atype.text.strip() == str(_user.AccountType) + assert suspended.text.strip() == "Active" + assert real_name.text == (_user.RealName or None) + assert irc_nick.text == (_user.IRCNick or None) + assert pgp_key.text == (_user.PGPKey or None) + + edit = edit.xpath("./a") + if user.can_edit_user(_user): + edit = next(iter(edit)) + assert edit.text.strip() == "Edit" + else: + assert not edit + + 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): + # 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}) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 1 + + row = next(iter(rows)) + username, type, status, realname, irc, pgp_key, edit = row + + username = next(iter(username.xpath("./a"))) + assert username.text.strip() == user.Username + + +def test_post_accounts_account_type(client: TestClient, user: User, + tu_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() + with db.begin(): + 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"}) + 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"}) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 1 + + row = next(iter(rows)) + username, type, status, realname, irc, pgp_key, edit = row + + assert type.text.strip() == "User" + + # Set our only user to a Trusted User. + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == TRUSTED_USER_ID + ).first() + + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"T": "t"}) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 1 + + row = next(iter(rows)) + username, type, status, realname, irc, pgp_key, edit = row + + assert type.text.strip() == "Trusted User" + + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == DEVELOPER_ID + ).first() + + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"T": "d"}) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 1 + + row = next(iter(rows)) + username, type, status, realname, irc, pgp_key, edit = row + + assert type.text.strip() == "Developer" + + with db.begin(): + user.AccountType = query(AccountType).filter( + AccountType.ID == TRUSTED_USER_AND_DEV_ID + ).first() + + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"T": "td"}) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 1 + + row = next(iter(rows)) + username, type, status, realname, irc, pgp_key, edit = row + + assert type.text.strip() == "Trusted User & Developer" + + +def test_post_accounts_status(client: TestClient, user: User, tu_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) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 1 + + row = next(iter(rows)) + username, type, status, realname, irc, pgp_key, edit = row + assert status.text.strip() == "Active" + + with db.begin(): + user.Suspended = True + + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"S": True}) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 1 + + row = next(iter(rows)) + username, type, status, realname, irc, pgp_key, edit = row + assert status.text.strip() == "Suspended" + + +def test_post_accounts_email(client: TestClient, user: User, tu_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}) + 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): + # 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}) + 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): + # 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}) + 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): + # Create a second user so we can compare sorts. + with db.begin(): + user_ = create_user("test2") + user_.AccountTypeID = DEVELOPER_ID + + sid = user.login(Request(), "testPassword") + cookies = {"AURSID": sid} + + # Show that "u" is the default search order, by username. + with client as request: + response = request.post("/accounts", cookies=cookies) + 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"}) + 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] + + # 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"}) + assert response.status_code == int(HTTPStatus.OK) + rows = get_rows(response.text) + assert len(rows) == 2 + + # Test the rows are reversed when ordering by IRCNick. + assert compare_text_values(4, first_rows, reversed(rows)) is True + + # Sort by "i" -> RealName. + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"SB": "r"}) + assert response.status_code == int(HTTPStatus.OK) + rows = get_rows(response.text) + assert len(rows) == 2 + + # Test the rows are reversed when ordering by RealName. + 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() + + # Fetch first_rows again with our new AccountType ordering. + with client as request: + response = request.post("/accounts", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + rows = get_rows(response.text) + assert len(rows) == 2 + first_rows = rows + + # Sort by "t" -> AccountType. + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"SB": "t"}) + assert response.status_code == int(HTTPStatus.OK) + rows = get_rows(response.text) + assert len(rows) == 2 + + # Test that rows again got reversed. + assert compare_text_values(1, first_rows, reversed(rows)) + + +def test_post_accounts_pgp_key(client: TestClient, user: User, tu_user: User): + with db.begin(): + user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" + + sid = user.login(Request(), "testPassword") + cookies = {"AURSID": sid} + + # Search via PGPKey. + with client as request: + response = request.post("/accounts", cookies=cookies, + 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): + # Create 150 users. + users = [user] + 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) + users.append(_user) + + sid = user.login(Request(), "testPassword") + cookies = {"AURSID": sid} + + with client as request: + response = request.post("/accounts", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 50 # `pp`, or hits per page is defined at 50. + + # Sort users in ascending default sort by order. + sorted_users = sorted(users, key=lambda u: u.Username) + + # Get the first fifty sorted users and assert that's what + # we got in the first search result page. + first_fifty = sorted_users[:50] + + for i, _user in enumerate(first_fifty): + row = rows[i] + username = row[0].xpath("./a")[0] # First column + assert username.text.strip() == _user.Username + + root = parse_root(response.text) + page_prev = root.xpath('//button[contains(@class, "page-prev")]')[0] + page_next = root.xpath('//button[contains(@class, "page-next")]')[0] + + assert page_prev.attrib["disabled"] == "disabled" + assert "disabled" not in page_next.attrib + + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"O": 50}) # +50 offset. + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 50 + + second_fifty = sorted_users[50:100] + + for i, _user in enumerate(second_fifty): + row = rows[i] + username = row[0].xpath("./a")[0] # First column + assert username.text.strip() == _user.Username + + with client as request: + response = request.post("/accounts", cookies=cookies, + data={"O": 101}) # Last page. + assert response.status_code == int(HTTPStatus.OK) + + rows = get_rows(response.text) + assert len(rows) == 50 + + root = parse_root(response.text) + page_prev = root.xpath('//button[contains(@class, "page-prev")]')[0] + page_next = root.xpath('//button[contains(@class, "page-next")]')[0] + + assert "disabled" not in page_prev.attrib + assert page_next.attrib["disabled"] == "disabled" + + +def test_get_terms_of_service(client: TestClient, user: User): + with db.begin(): + term = create(Term, Description="Test term.", + URL="http://localhost", Revision=1) + + with client as request: + response = request.get("/tos", allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + request = Request() + sid = user.login(request, "testPassword") + cookies = {"AURSID": sid} + + # 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) + 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) + assert response.status_code == int(HTTPStatus.OK) + + with db.begin(): + accepted_term = create(AcceptedTerm, User=user, + Term=term, Revision=term.Revision) + + with client as request: + response = request.get("/tos", cookies=cookies, allow_redirects=False) + # We accepted the term, there's nothing left to accept. + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + # Bump the term's revision. + with db.begin(): + term.Revision = 2 + + with client as request: + response = request.get("/tos", cookies=cookies, allow_redirects=False) + # 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) + + with db.begin(): + accepted_term.Revision = term.Revision + + with client as request: + response = request.get("/tos", cookies=cookies, allow_redirects=False) + # We updated the term revision, there's nothing left to accept. + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_post_terms_of_service(client: TestClient, user: User): + request = Request() + sid = user.login(request, "testPassword") + + data = {"accept": True} # POST data. + cookies = {"AURSID": sid} # Auth cookie. + + # Create a fresh Term. + with db.begin(): + 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) + 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) + 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) + 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() + assert accepted_term.User == user + assert accepted_term.Term == term + + # Update the term to revision 2. + with db.begin(): + term.Revision = 2 + + # A GET request gives us the new revision to accept. + with client as request: + response = request.get("/tos", cookies=cookies) + 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) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + # Check that the records ended up matching. + assert accepted_term.Revision == term.Revision + + # 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) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + +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) + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/" diff --git a/test/test_adduser.py b/test/test_adduser.py new file mode 100644 index 00000000..6c71a519 --- /dev/null +++ b/test/test_adduser.py @@ -0,0 +1,56 @@ +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") + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def run_main(args: List[str] = []): + with mock.patch("sys.argv", ["aurweb-adduser"] + args): + adduser.main() + + +def test_adduser_no_args(): + with pytest.raises(SystemExit): + run_main() + + +def test_adduser(): + run_main(["-u", "test", "-e", "test@example.org", "-p", "abcd1234"]) + test = db.query(User).filter(User.Username == "test").first() + assert test is not None + assert test.login(Request(), "abcd1234") + + +def test_adduser_tu(): + run_main([ + "-u", "test", "-e", "test@example.org", "-p", "abcd1234", + "-t", at.TRUSTED_USER + ]) + test = db.query(User).filter(User.Username == "test").first() + assert test is not None + assert test.AccountTypeID == at.TRUSTED_USER_ID + + +def test_adduser_ssh_pk(): + 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_key.PubKey) diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py new file mode 100644 index 00000000..82805ecf --- /dev/null +++ b/test/test_api_rate_limit.py @@ -0,0 +1,36 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.api_rate_limit import ApiRateLimit + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_api_rate_key_creation(): + with db.begin(): + 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 + + +def test_api_rate_key_ip_default(): + with db.begin(): + api_rate_limit = db.create(ApiRateLimit, Requests=10, WindowStart=1) + assert api_rate_limit.IP == str() + + +def test_api_rate_key_null_requests_raises_exception(): + with pytest.raises(IntegrityError): + ApiRateLimit(IP="127.0.0.1", WindowStart=1) + + +def test_api_rate_key_null_window_start_raises_exception(): + with pytest.raises(IntegrityError): + ApiRateLimit(IP="127.0.0.1", Requests=1) diff --git a/test/test_asgi.py b/test/test_asgi.py new file mode 100644 index 00000000..af13266f --- /dev/null +++ b/test/test_asgi.py @@ -0,0 +1,119 @@ +import http +import os +import re + +from unittest import mock + +import fastapi +import pytest + +from fastapi import HTTPException +from fastapi.testclient import TestClient + +import aurweb.asgi +import aurweb.config +import aurweb.redis + +from aurweb.testing.email import Email +from aurweb.testing.requests import Request + + +@pytest.fixture +def setup(db_test, email_test): + return + + +@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. """ + + redis_addr = aurweb.config.get("options", "redis_address") + + def mock_get(section: str, key: str): + if section == "fastapi" and key == "session_secret": + return None + return redis_addr + + with mock.patch("aurweb.config.get", side_effect=mock_get): + with pytest.raises(Exception): + await aurweb.asgi.app_startup() + + +@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() + + +@pytest.mark.asyncio +async def test_asgi_http_exception_handler(): + exc = HTTPException(status_code=422, detail="EXCEPTION!") + phrase = http.HTTPStatus(exc.status_code).phrase + response = await aurweb.asgi.http_exception_handler(Request(), exc) + assert response.status_code == 422 + content = response.body.decode() + assert f"{exc.status_code} - {phrase}" in content + assert "EXCEPTION!" in content + + +@pytest.mark.asyncio +async def test_asgi_app_unsupported_backends(): + config_get = aurweb.config.get + + # Test that the previously supported "sqlite" backend is now + # unsupported by FastAPI. + def mock_sqlite_backend(section: str, key: str): + if section == "database" and key == "backend": + return "sqlite" + return config_get(section, key) + + with mock.patch("aurweb.config.get", side_effect=mock_sqlite_backend): + expr = r"^.*\(sqlite\) is unsupported.*$" + with pytest.raises(ValueError, match=expr): + await aurweb.asgi.app_startup() + + +def test_internal_server_error(setup: None, + caplog: pytest.LogCaptureFixture): + config_getboolean = aurweb.config.getboolean + + def mock_getboolean(section: str, key: str) -> bool: + if section == "options" and key == "traceback": + return True + return config_getboolean(section, key) + + @aurweb.asgi.app.get("/internal_server_error") + async def internal_server_error(request: fastapi.Request): + raise ValueError("test exception") + + with mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean): + with TestClient(app=aurweb.asgi.app) as request: + resp = request.get("/internal_server_error") + assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR) + + # Let's assert that a notification was sent out to the postmaster. + assert Email.count() == 1 + + aur_location = aurweb.config.get("options", "aur_location") + email = Email(1) + assert f"Location: {aur_location}" in email.body + assert "Traceback ID:" in email.body + assert "Version:" in email.body + assert "Datetime:" in email.body + assert f"[1] {aur_location}" in email.body + + # Assert that the exception got logged with with its traceback id. + expr = r"FATAL\[.{7}\]" + assert re.search(expr, caplog.text) + + # Let's do it again; no email should be sent the next time, + # since the hash is stored in redis. + with mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean): + with TestClient(app=aurweb.asgi.app) as request: + resp = request.get("/internal_server_error") + assert resp.status_code == int(http.HTTPStatus.INTERNAL_SERVER_ERROR) + assert Email.count() == 1 diff --git a/test/test_aurblup.py b/test/test_aurblup.py new file mode 100644 index 00000000..0b499d57 --- /dev/null +++ b/test/test_aurblup.py @@ -0,0 +1,91 @@ +import tempfile + +from unittest import mock + +import py +import pytest + +from aurweb import config, db +from aurweb.models import OfficialProvider +from aurweb.scripts import aurblup +from aurweb.testing.alpm import AlpmDatabase + + +@pytest.fixture +def tempdir() -> str: + with tempfile.TemporaryDirectory() as name: + yield name + + +@pytest.fixture +def alpm_db(tempdir: py.path.local) -> AlpmDatabase: + yield AlpmDatabase(tempdir) + + +@pytest.fixture(autouse=True) +def setup(db_test, alpm_db: AlpmDatabase, tempdir: py.path.local) -> None: + config_get = config.get + + def mock_config_get(section: str, key: str) -> str: + value = config_get(section, key) + if section == "aurblup": + if key == "db-path": + return alpm_db.local + elif key == "server": + return f'file://{alpm_db.remote}' + elif key == "sync-dbs": + return alpm_db.repo + return value + + with mock.patch("aurweb.config.get", side_effect=mock_config_get): + config.rehash() + yield + config.rehash() + + +def test_aurblup(alpm_db: AlpmDatabase): + # Test that we can add a package. + alpm_db.add("pkg", "1.0", "x86_64", provides=["pkg2", "pkg3"]) + alpm_db.add("pkg2", "2.0", "x86_64") + aurblup.main() + + # Test that the package got added to the database. + for name in ("pkg", "pkg2"): + pkg = db.query(OfficialProvider).filter( + OfficialProvider.Name == name).first() + assert pkg is not None + + # Test that we can remove the package. + alpm_db.remove("pkg") + + # Run aurblup again with forced repository update. + aurblup.main(True) + + # Expect that the database got updated accordingly. + pkg = db.query(OfficialProvider).filter( + OfficialProvider.Name == "pkg").first() + assert pkg is None + pkg2 = db.query(OfficialProvider).filter( + OfficialProvider.Name == "pkg2").first() + assert pkg2 is not None + + +def test_aurblup_cleanup(alpm_db: AlpmDatabase): + # Add a package and sync up the database. + alpm_db.add("pkg", "1.0", "x86_64", provides=["pkg2", "pkg3"]) + aurblup.main() + + # 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") + + # 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() + assert len(providers) == 0 diff --git a/test/test_auth.py b/test/test_auth.py new file mode 100644 index 00000000..b8221c19 --- /dev/null +++ b/test/test_auth.py @@ -0,0 +1,153 @@ +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.models.account_type import USER, USER_ID +from aurweb.models.session import Session +from aurweb.models.user import User +from aurweb.testing.requests import Request + + +@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.com", + RealName="Test User", Passwd="testPassword", + AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def backend() -> BasicAuthBackend: + yield BasicAuthBackend() + + +@pytest.mark.asyncio +async def test_auth_backend_missing_sid(backend: BasicAuthBackend): + # The request has no AURSID cookie, so authentication fails, and + # AnonymousUser is returned. + _, result = await backend.authenticate(Request()) + assert not result.is_authenticated() + + +@pytest.mark.asyncio +async def test_auth_backend_invalid_sid(backend: BasicAuthBackend): + # Provide a fake AURSID that won't be found in the database. + # This results in our path going down the invalid sid route, + # which gives us an AnonymousUser. + request = Request() + request.cookies["AURSID"] = "fake" + _, result = await backend.authenticate(request) + assert not result.is_authenticated() + + +@pytest.mark.asyncio +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) + + +@pytest.mark.asyncio +async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): + # This time, everything matches up. We expect the user to + # equal the real_user. + now_ts = time.utcnow() + with db.begin(): + db.create(Session, UsersID=user.ID, SessionID="realSession", + LastUpdateTS=now_ts + 5) + + request = Request() + request.cookies["AURSID"] = "realSession" + _, result = await backend.authenticate(request) + assert result == user + + +@pytest.mark.asyncio +async def test_expired_session(backend: BasicAuthBackend, user: User): + """ Login, expire the session manually, then authenticate. """ + # First, build a Request with a logged in user. + request = Request() + request.user = user + sid = request.user.login(Request(), "testPassword") + request.cookies["AURSID"] = sid + + # Set Session.LastUpdateTS to 20 seconds expired. + timeout = config.getint("options", "login_timeout") + now_ts = time.utcnow() + with db.begin(): + request.user.session.LastUpdateTS = now_ts - timeout - 20 + + # Run through authentication backend and get the session + # deleted due to its expiration. + await backend.authenticate(request) + session = db.query(Session).filter(Session.SessionID == sid).first() + assert session is None + + +@pytest.mark.asyncio +async def test_auth_required_redirection_bad_referrer(): + # Create a fake route function which can be wrapped by auth_required. + def bad_referrer_route(request: fastapi.Request): + pass + + # Get down to the nitty gritty internal wrapper. + bad_referrer_route = _auth_required()(bad_referrer_route) + + # Execute the route with a "./blahblahblah" Referer, which does not + # match aur_location; `./` has been used as a prefix to attempt to + # ensure we're providing a fake referer. + with pytest.raises(HTTPException) as exc: + request = Request(method="POST", headers={"Referer": "./blahblahblah"}) + await bad_referrer_route(request) + assert exc.detail == "Bad Referer header." + + +def test_account_type_required(): + """ This test merely asserts that a few different paths + do not raise exceptions. """ + # This one shouldn't raise. + account_type_required({USER}) + + # This one also shouldn't raise. + account_type_required({USER_ID}) + + # But this one should! We have no "FAKE" key. + with pytest.raises(KeyError): + account_type_required({'FAKE'}) + + +def test_is_trusted_user(): + user_ = AnonymousUser() + assert not user_.is_trusted_user() + + +def test_is_developer(): + user_ = AnonymousUser() + assert not user_.is_developer() + + +def test_is_elevated(): + user_ = AnonymousUser() + assert not user_.is_elevated() + + +def test_voted_for(): + user_ = AnonymousUser() + assert not user_.voted_for(None) + + +def test_notified(): + user_ = AnonymousUser() + assert not user_.notified(None) diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py new file mode 100644 index 00000000..4136a18e --- /dev/null +++ b/test/test_auth_routes.py @@ -0,0 +1,335 @@ +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 +from aurweb.models.session import Session +from aurweb.models.user import User + +# Some test global constants. +TEST_USERNAME = "test" +TEST_EMAIL = "test@example.org" +TEST_REFERER = { + "referer": aurweb.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) + 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_login_logout(client: TestClient, user: User): + 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) + 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) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + response = request.post("/logout", data=post_data, cookies={ + "AURSID": response.cookies.get("AURSID") + }, allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + assert "AURSID" not in response.cookies + + +def mock_getboolean(a, b): + if a == "options" and b == "disable_http_login": + return True + return bool(aurweb.config.get(a, b)) + + +@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 + 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. """ + + # Create a local TestClient here since we mocked configuration. + # client = TestClient(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) + + # Data used for our upcoming http post request. + 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) + + # 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") + 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.value is not None and len(cookie.value) > 0 + + # Let's make sure we actually have a session relationship + # with the AURSID we ended up with. + record = db.query(Session, Session.SessionID == cookie.value).first() + assert record is not None and record.User == user + assert user.session == record + + +def test_authenticated_login(client: TestClient, user: User): + 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) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/" + + # Now, let's verify that we get the logged in rendering + # 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) + assert response.status_code == int(HTTPStatus.OK) + assert "Logged-in as: test" in response.text + + +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) + 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": "/" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + # Make sure password isn't prefilled and remember_me isn't checked. + content = response.content.decode() + assert post_data["passwd"] not in content + assert "checked" not in content + + +def test_login_remember_me(client: TestClient, user: User): + post_data = { + "user": "test", + "passwd": "testPassword", + "next": "/", + "remember_me": True + } + + with client as request: + response = request.post("/login", data=post_data, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert "AURSID" in response.cookies + + cookie_timeout = aurweb.config.getint( + "options", "persistent_cookie_timeout") + now_ts = time.utcnow() + session = db.query(Session).filter(Session.UsersID == user.ID).first() + + # Expect that LastUpdateTS is not past the cookie timeout + # for a remembered session. + assert session.LastUpdateTS > (now_ts - cookie_timeout) + + +def test_login_incorrect_password_remember_me(client: TestClient, user: User): + post_data = { + "user": "test", + "passwd": "badPassword", + "next": "/", + "remember_me": "on" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + # Make sure username is prefilled, password isn't prefilled, + # and remember_me is checked. + assert post_data["user"] in response.text + assert post_data["passwd"] not in response.text + assert "checked" in response.text + + +def test_login_missing_password(client: TestClient): + post_data = { + "user": "test", + "next": "/" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + # Make sure username is prefilled and remember_me isn't checked. + assert post_data["user"] in response.text + assert "checked" not in response.text + + +def test_login_incorrect_password(client: TestClient): + post_data = { + "user": "test", + "passwd": "badPassword", + "next": "/" + } + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + # Make sure username is prefilled, password isn't prefilled + # and remember_me isn't checked. + assert post_data["user"] in response.text + assert post_data["passwd"] not in response.text + assert "checked" not in response.text + + +def test_login_bad_referer(client: TestClient): + post_data = { + "user": "test", + "passwd": "testPassword", + "next": "/", + } + + # Create new TestClient without a Referer header. + client = TestClient(app) + + with client as request: + response = request.post("/login", data=post_data) + assert "AURSID" not in response.cookies + + BAD_REFERER = { + "referer": aurweb.config.get("options", "aur_location") + ".mal.local", + } + with client as request: + response = request.post("/login", data=post_data, headers=BAD_REFERER) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + assert "AURSID" not in response.cookies + + +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 + to login as `user2` and expect the internal server error rendering + by our error handler. + + This exercises the bad path of /login, where we can't find a unique + SID to assign the 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) + + # Create a session with ID == "testSession" for `user`. + 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. + def mock_generate_sid(): + return "testSession" + + # Login as `user2`; we expect an internal server error response + # with a relevent detail. + post_data = { + "user": user2.Username, + "passwd": "testPassword", + "next": "/", + } + generate_unique_sid_ = "aurweb.models.session.generate_unique_sid" + 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={}) + assert response.status_code == int(HTTPStatus.INTERNAL_SERVER_ERROR) + + assert "500 - Internal Server Error" in response.text + + # Make sure an IntegrityError from the DB got logged out + # with a FATAL traceback ID. + expr = r"FATAL\[.{7}\]" + assert re.search(expr, caplog.text) + assert "IntegrityError" in caplog.text + + expr = r"Duplicate entry .+ for key .+SessionID.+" + assert re.search(expr, response.text) diff --git a/test/test_ban.py b/test/test_ban.py new file mode 100644 index 00000000..ff49f7e2 --- /dev/null +++ b/test/test_ban.py @@ -0,0 +1,58 @@ +import warnings + +from datetime import datetime, timedelta + +import pytest + +from sqlalchemy import exc as sa_exc + +from aurweb import db +from aurweb.db import create +from aurweb.models.ban import Ban, is_banned +from aurweb.testing.requests import Request + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def ban() -> Ban: + ts = datetime.utcnow() + timedelta(seconds=30) + with db.begin(): + ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) + yield ban + + +def test_ban(ban: Ban): + assert ban.IPAddress == "127.0.0.1" + assert bool(ban.BanTS) + + +def test_invalid_ban(): + with pytest.raises(sa_exc.IntegrityError): + bad_ban = Ban(BanTS=datetime.utcnow()) + + # We're adding a ban with no primary key; this causes an + # SQLAlchemy warnings when committing to the DB. + # Ignore them. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", sa_exc.SAWarning) + with db.begin(): + db.add(bad_ban) + + # Since we got a transaction failure, we need to rollback. + db.rollback() + + +def test_banned(ban: Ban): + request = Request() + request.client.host = "127.0.0.1" + assert is_banned(request) + + +def test_not_banned(ban: Ban): + request = Request() + request.client.host = "192.168.0.1" + assert not is_banned(request) diff --git a/test/test_cache.py b/test/test_cache.py new file mode 100644 index 00000000..b49ee386 --- /dev/null +++ b/test/test_cache.py @@ -0,0 +1,71 @@ +import pytest + +from aurweb import cache, db +from aurweb.models.account_type import USER_ID +from aurweb.models.user import User + + +@pytest.fixture(autouse=True) +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() + + +@pytest.mark.asyncio +async def test_db_count_cache(redis): + db.create(User, Username="user1", + Email="user1@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID) + + 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() + + # It's cached now. + assert await cache.db_count_cache(redis, "key1", query) == query.count() + + +@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) + + # Cache a query with an expire. + value = await cache.db_count_cache(redis, "key1", query, 100) + assert value == query.count() + + assert redis.expires["key1"] == 100 diff --git a/test/test_captcha.py b/test/test_captcha.py new file mode 100644 index 00000000..e5f8c71a --- /dev/null +++ b/test/test_captcha.py @@ -0,0 +1,67 @@ +import hashlib + +import pytest + +from aurweb import captcha + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_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. """ + salts = captcha.get_captcha_salts() + salt = salts[0] + + token1 = captcha.get_captcha_token(salt) + token2 = hashlib.md5(salt.encode()).hexdigest()[:3] + + assert token1 == token2 + + +def test_captcha_challenge_answer(): + """ Make sure that executing the captcha challenge via shell + produces the correct result by comparing it against a straight + up token conversion. """ + salts = captcha.get_captcha_salts() + salt = salts[0] + + challenge = captcha.get_captcha_challenge(salt) + + token = captcha.get_captcha_token(salt) + challenge2 = f"LC_ALL=C pacman -V|sed -r 's#[0-9]+#{token}#g'|md5sum|cut -c1-6" + + assert challenge == challenge2 + + +def test_captcha_salt_filter(): + """ Make sure captcha_salt_filter returns the first salt from + get_captcha_salts(). + + Example usage: + + """ + salt = captcha.captcha_salt_filter(None) + assert salt == captcha.get_captcha_salts()[0] + + +def test_captcha_cmdline_filter(): + """ Make sure that the captcha_cmdline filter gives us the + same challenge that get_captcha_challenge does. + + Example usage: + {{ captcha_salt | captcha_cmdline }} + """ + salt = captcha.captcha_salt_filter(None) + display1 = captcha.captcha_cmdline_filter(None, salt) + display2 = captcha.get_captcha_challenge(salt) + assert display1 == display2 diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 00000000..b78f477c --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,177 @@ +import configparser +import io +import os +import re + +from unittest import mock + +import py + +from aurweb import config +from aurweb.scripts.config import main + + +def noop(*args, **kwargs) -> None: + return + + +def test_get(): + assert config.get("options", "disable_http_login") == "0" + + +def test_getboolean(): + assert not config.getboolean("options", "disable_http_login") + + +def test_getint(): + assert config.getint("options", "disable_http_login") == 0 + + +def mock_config_get(): + config_get = config.get + + def _mock_config_get(section: str, option: str): + if section == "options": + if option == "salt_rounds": + return "666" + return config_get(section, option) + return _mock_config_get + + +@mock.patch("aurweb.config.get", side_effect=mock_config_get()) +def test_config_main_get(get: str): + stdout = io.StringIO() + args = ["aurweb-config", "get", "options", "salt_rounds"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stdout", stdout): + main() + + expected = "666" + assert stdout.getvalue().strip() == expected + + +@mock.patch("aurweb.config.get", side_effect=mock_config_get()) +def test_config_main_get_unknown_section(get: str): + stderr = io.StringIO() + args = ["aurweb-config", "get", "fakeblahblah", "salt_rounds"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stderr", stderr): + main() + + # With an invalid section, we should get a usage error. + expected = r'^error: no section found$' + assert re.match(expected, stderr.getvalue().strip()) + + +@mock.patch("aurweb.config.get", side_effect=mock_config_get()) +def test_config_main_get_unknown_option(get: str): + stderr = io.StringIO() + args = ["aurweb-config", "get", "options", "fakeblahblah"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stderr", stderr): + main() + + expected = "error: no option found" + assert stderr.getvalue().strip() == expected + + +@mock.patch("aurweb.config.save", side_effect=noop) +def test_config_main_set(save: None): + data = None + + def set_option(section: str, option: str, value: str) -> None: + nonlocal data + data = value + + args = ["aurweb-config", "set", "options", "salt_rounds", "666"] + with mock.patch("sys.argv", args): + with mock.patch("aurweb.config.set_option", side_effect=set_option): + main() + + expected = "666" + assert data == expected + + +def test_config_main_set_real(tmpdir: py.path.local): + """ + Test a real set_option path. + """ + + # Copy AUR_CONFIG to {tmpdir}/aur.config. + aur_config = os.environ.get("AUR_CONFIG") + tmp_aur_config = os.path.join(str(tmpdir), "aur.config") + with open(aur_config) as f: + with open(tmp_aur_config, "w") as o: + o.write(f.read()) + + # Force reset the parser. This should NOT be done publicly. + config._parser = None + + value = 666 + args = ["aurweb-config", "set", "options", "fake-key", str(value)] + with mock.patch.dict("os.environ", {"AUR_CONFIG": tmp_aur_config}): + with mock.patch("sys.argv", args): + # Run aurweb.config.main(). + main() + + # Update the config; fake-key should be set. + config.rehash() + assert config.getint("options", "fake-key") == 666 + + # Restore config back to normal. + args = ["aurweb-config", "unset", "options", "fake-key"] + with mock.patch("sys.argv", args): + main() + + # Return the config back to normal. + config.rehash() + + # fake-key should no longer exist. + assert config.getint("options", "fake-key") is None + + +def test_config_main_set_immutable(): + data = None + + def mock_set_option(section: str, option: str, value: str) -> None: + nonlocal data + data = value + + 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): + main() + + expected = None + assert data == expected + + +def test_config_main_set_invalid_value(): + stderr = io.StringIO() + + args = ["aurweb-config", "set", "options", "salt_rounds"] + with mock.patch("sys.argv", args): + with mock.patch("sys.stderr", stderr): + main() + + expected = "error: no value provided" + assert stderr.getvalue().strip() == expected + + +@ mock.patch("aurweb.config.save", side_effect=noop) +def test_config_main_set_unknown_section(save: None): + stderr = io.StringIO() + + def mock_set_option(section: str, option: str, value: str) -> None: + raise configparser.NoSectionError(section=section) + + 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): + main() + + assert stderr.getvalue().strip() == "error: no section found" diff --git a/test/test_db.py b/test/test_db.py new file mode 100644 index 00000000..f36fff2c --- /dev/null +++ b/test/test_db.py @@ -0,0 +1,224 @@ +import os +import re +import sqlite3 +import tempfile + +from unittest import mock + +import pytest + +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. """ + use_alembic = True + verbose = True + + +class DBCursor: + """ A fake database cursor object used in tests. """ + items = [] + + def execute(self, *args, **kwargs): + self.items = list(args) + return self + + def fetchall(self): + return self.items + + +class DBConnection: + """ A fake database connection object used in tests. """ + @staticmethod + def cursor(): + return DBCursor() + + @staticmethod + def create_function(name, num_args, func): + pass + + +def make_temp_config(*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) + """ + aurwebdir = aurweb.config.get("options", "aurwebdir") + config_file = os.path.join(aurwebdir, "conf", "config.dev") + config_defaults = os.path.join(aurwebdir, "conf", "config.defaults") + + db_name = aurweb.config.get("database", "name") + db_host = aurweb.config.get_with_fallback("database", "host", "localhost") + db_port = aurweb.config.get_with_fallback("database", "port", "3306") + db_user = aurweb.config.get_with_fallback("database", "user", "root") + db_password = aurweb.config.get_with_fallback("database", "password", None) + + # Replacements to perform before *replacements. + # These serve as generic replacements in config.dev + perform = ( + (r"name = .+", f"name = {db_name}"), + (r"host = .+", f"host = {db_host}"), + (r";port = .+", f";port = {db_port}"), + (r"user = .+", f"user = {db_user}"), + (r"password = .+", f"password = {db_password}"), + ("YOUR_AUR_ROOT", aurwebdir), + ) + + tmpdir = tempfile.TemporaryDirectory() + tmp = os.path.join(tmpdir.name, "config.tmp") + with open(config_file) as f: + config = f.read() + for repl in tuple(perform + replacements): + config = re.sub(repl[0], repl[1], config) + with open(tmp, "w") as o: + o.write(config) + with open(config_defaults) as i: + with open(f"{tmp}.defaults", "w") as o: + o.write(i.read()) + return tmpdir, tmp + + +def make_temp_sqlite_config(): + 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")) + + +@pytest.fixture(autouse=True) +def setup(db_test): + if os.path.exists("/tmp/aurweb.sqlite3"): + os.remove("/tmp/aurweb.sqlite3") + + +def test_sqlalchemy_sqlite_url(): + tmpctx, tmp = make_temp_sqlite_config() + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def test_sqlalchemy_mysql_url(): + tmpctx, tmp = make_temp_mysql_config() + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def test_sqlalchemy_mysql_port_url(): + tmpctx, tmp = make_temp_config((r";port = 3306", "port = 3306")) + + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def test_sqlalchemy_mysql_socket_url(): + tmpctx, tmp = make_temp_config() + + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + assert db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def test_sqlalchemy_unknown_backend(): + tmpctx, tmp = make_temp_config((r"backend = .+", "backend = blah")) + + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + with pytest.raises(ValueError): + db.get_sqlalchemy_url() + aurweb.config.rehash() + + +def test_db_connects_without_fail(): + """ This only tests the actual config supplied to pytest. """ + db.connect() + + +def test_connection_class_unsupported_backend(): + tmpctx, tmp = make_temp_config((r"backend = .+", "backend = blah")) + + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + with pytest.raises(ValueError): + db.Connection() + aurweb.config.rehash() + + +@mock.patch("MySQLdb.connect", mock.MagicMock(return_value=True)) +def test_connection_mysql(): + tmpctx, tmp = make_temp_mysql_config() + with tmpctx: + with mock.patch.dict(os.environ, {"AUR_CONFIG": tmp}): + aurweb.config.rehash() + db.Connection() + aurweb.config.rehash() + + +def test_create_delete(): + with db.begin(): + account_type = db.create(AccountType, AccountType="test") + + record = db.query(AccountType, AccountType.AccountType == "test").first() + assert record is not None + + with db.begin(): + db.delete(account_type) + + record = db.query(AccountType, AccountType.AccountType == "test").first() + assert record is None + + +def test_add_commit(): + # Use db.add and db.commit to add a temporary record. + account_type = AccountType(AccountType="test") + with db.begin(): + db.add(account_type) + + # Assert it got created in the DB. + assert bool(account_type.ID) + + # Query the DB for it and compare the record with our object. + record = db.query(AccountType, AccountType.AccountType == "test").first() + assert record == account_type + + # Remove the record. + with db.begin(): + db.delete(account_type) + + +def test_connection_executor_mysql_paramstyle(): + executor = db.ConnectionExecutor(None, backend="mysql") + assert executor.paramstyle() == "format" + + +@mock.patch("sqlite3.paramstyle", "pyformat") +def test_connection_executor_sqlite_paramstyle(): + executor = db.ConnectionExecutor(None, backend="sqlite") + assert executor.paramstyle() == sqlite3.paramstyle + + +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") diff --git a/test/test_defaults.py b/test/test_defaults.py new file mode 100644 index 00000000..4803fb5a --- /dev/null +++ b/test/test_defaults.py @@ -0,0 +1,14 @@ +from aurweb import defaults + + +def test_fallback_pp(): + assert defaults.fallback_pp(75) == defaults.PP + assert defaults.fallback_pp(100) == 100 + + +def test_pp(): + assert defaults.PP == 50 + + +def test_o(): + assert defaults.O == 0 diff --git a/test/test_dependency_type.py b/test/test_dependency_type.py new file mode 100644 index 00000000..c5afd38d --- /dev/null +++ b/test/test_dependency_type.py @@ -0,0 +1,34 @@ +import pytest + +from aurweb.db import begin, create, delete, query +from aurweb.models.dependency_type import DependencyType + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +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() + assert dependency_type is not None + + +def test_dependency_type_creation(): + with begin(): + dependency_type = create(DependencyType, Name="Test Type") + assert bool(dependency_type.ID) + assert dependency_type.Name == "Test Type" + with begin(): + delete(dependency_type) + + +def test_dependency_type_null_name_uses_default(): + with begin(): + dependency_type = create(DependencyType) + assert dependency_type.Name == str() + with begin(): + delete(dependency_type) diff --git a/test/test_email.py b/test/test_email.py new file mode 100644 index 00000000..873feffe --- /dev/null +++ b/test/test_email.py @@ -0,0 +1,59 @@ +import io + +from subprocess import PIPE, Popen + +import pytest + +from aurweb import config +from aurweb.testing.email import Email + + +@pytest.fixture(autouse=True) +def setup(email_test): + return + + +def sendmail(from_: str, to_: str, content: str) -> Email: + binary = config.get("notifications", "sendmail") + proc = Popen(binary, stdin=PIPE, stdout=PIPE, stderr=PIPE) + content = f"From: {from_}\nTo: {to_}\n\n{content}" + proc.communicate(content.encode()) + proc.wait() + assert proc.returncode == 0 + + +def test_email_glue(): + """ 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 + + email1 = Email(1) + email2 = Email(1) + assert email1.glue() == email2.glue() + + +def test_email_dump(): + """ Test that Email.dump() dumps a single email. """ + body = "Test email." + sendmail("test@example.org", "test@example.org", body) + assert Email.count() == 1 + + stdout = io.StringIO() + Email.dump(file=stdout) + content = stdout.getvalue() + assert "== Email #1 ==" in content + + +def test_email_dump_multiple(): + """ 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) + assert Email.count() == 2 + + stdout = io.StringIO() + Email.dump(file=stdout) + content = stdout.getvalue() + assert "== Email #1 ==" in content + assert "== Email #2 ==" in content diff --git a/test/test_exceptions.py b/test/test_exceptions.py new file mode 100644 index 00000000..e43cd645 --- /dev/null +++ b/test/test_exceptions.py @@ -0,0 +1,106 @@ +from aurweb import exceptions + + +def test_aurweb_exception(): + try: + raise exceptions.AurwebException("test") + except exceptions.AurwebException as exc: + assert str(exc) == "test" + + +def test_maintenance_exception(): + try: + raise exceptions.MaintenanceException("test") + except exceptions.MaintenanceException as exc: + assert str(exc) == "test" + + +def test_banned_exception(): + try: + raise exceptions.BannedException("test") + except exceptions.BannedException as exc: + assert str(exc) == "test" + + +def test_already_voted_exception(): + try: + raise exceptions.AlreadyVotedException("test") + except exceptions.AlreadyVotedException as exc: + assert str(exc) == "already voted for package base: test" + + +def test_broken_update_hook_exception(): + try: + raise exceptions.BrokenUpdateHookException("test") + except exceptions.BrokenUpdateHookException as exc: + assert str(exc) == "broken update hook: test" + + +def test_invalid_arguments_exception(): + try: + raise exceptions.InvalidArgumentsException("test") + except exceptions.InvalidArgumentsException as exc: + assert str(exc) == "test" + + +def test_invalid_packagebase_exception(): + try: + raise exceptions.InvalidPackageBaseException("test") + except exceptions.InvalidPackageBaseException as exc: + assert str(exc) == "package base not found: test" + + +def test_invalid_comment_exception(): + try: + raise exceptions.InvalidCommentException("test") + except exceptions.InvalidCommentException as exc: + assert str(exc) == "comment is too short: test" + + +def test_invalid_reason_exception(): + try: + raise exceptions.InvalidReasonException("test") + except exceptions.InvalidReasonException as exc: + assert str(exc) == "invalid reason: test" + + +def test_invalid_user_exception(): + try: + raise exceptions.InvalidUserException("test") + except exceptions.InvalidUserException as exc: + assert str(exc) == "unknown user: test" + + +def test_not_voted_exception(): + try: + raise exceptions.NotVotedException("test") + except exceptions.NotVotedException as exc: + assert str(exc) == "missing vote for package base: test" + + +def test_packagebase_exists_exception(): + try: + raise exceptions.PackageBaseExistsException("test") + except exceptions.PackageBaseExistsException as exc: + assert str(exc) == "package base already exists: test" + + +def test_permission_denied_exception(): + try: + raise exceptions.PermissionDeniedException("test") + except exceptions.PermissionDeniedException as exc: + assert str(exc) == "permission denied: test" + + +def test_repository_name_exception(): + try: + raise exceptions.InvalidRepositoryNameException("test") + except exceptions.InvalidRepositoryNameException as exc: + assert str(exc) == "invalid repository name: test" + + +def test_invariant_error(): + try: + raise exceptions.InvariantError("test") + except exceptions.InvariantError as exc: + assert str(exc) == "test" diff --git a/test/test_filelock.py b/test/test_filelock.py new file mode 100644 index 00000000..70aa7580 --- /dev/null +++ b/test/test_filelock.py @@ -0,0 +1,26 @@ +import py + +from _pytest.logging import LogCaptureFixture + +from aurweb.testing.filelock import FileLock + + +def test_filelock(tmpdir: py.path.local): + cb_path = None + + def setup(path: str): + nonlocal cb_path + cb_path = str(path) + + flock = FileLock(tmpdir, "test") + assert not flock.lock(on_create=setup) + assert cb_path == str(tmpdir / "test") + assert flock.lock() + + +def test_filelock_default(caplog: LogCaptureFixture, tmpdir: py.path.local): + # Test default_on_create here. + flock = FileLock(tmpdir, "test") + assert not flock.lock() + assert caplog.messages[0] == f"Filelock at {flock.path} acquired." + assert flock.lock() diff --git a/test/test_filters.py b/test/test_filters.py new file mode 100644 index 00000000..558911f5 --- /dev/null +++ b/test/test_filters.py @@ -0,0 +1,36 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +from aurweb import filters, time + + +def test_timestamp_to_datetime(): + ts = time.utcnow() + dt = datetime.utcfromtimestamp(int(ts)) + assert filters.timestamp_to_datetime(ts) == dt + + +def test_as_timezone(): + ts = time.utcnow() + dt = filters.timestamp_to_datetime(ts) + assert filters.as_timezone(dt, "UTC") == dt.astimezone(tz=ZoneInfo("UTC")) + + +def test_number_format(): + assert filters.number_format(0.222, 2) == "0.22" + assert filters.number_format(0.226, 2) == "0.23" + + +def test_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" + assert extended.get("b") == "d" + + +def test_to_qs(): + """ 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" diff --git a/test/test_group.py b/test/test_group.py new file mode 100644 index 00000000..82b82464 --- /dev/null +++ b/test/test_group.py @@ -0,0 +1,23 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.group import Group + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_group_creation(): + with db.begin(): + group = db.create(Group, Name="Test Group") + assert bool(group.ID) + assert group.Name == "Test Group" + + +def test_group_null_name_raises_exception(): + with pytest.raises(IntegrityError): + Group() diff --git a/test/test_homepage.py b/test/test_homepage.py new file mode 100644 index 00000000..5d3bc711 --- /dev/null +++ b/test/test_homepage.py @@ -0,0 +1,223 @@ +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.models.account_type import USER_ID +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +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 + +client = TestClient(app) + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def user(): + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd="testPassword", AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def redis(): + redis = redis_connection() + + 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"): + if redis.get(key) is not None: + redis.delete(key) + + delete_keys() + yield redis + delete_keys() + + +@pytest.fixture +def packages(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}. + pkgs = [] + 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) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + pkgs.append(pkg) + now += 1 + + yield pkgs + + +def test_homepage(): + with client as request: + response = request.get("/") + 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"} + get_ssh_fingerprints_mock.return_value = fingerprints + + 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() + + +@patch('aurweb.util.get_ssh_fingerprints') +def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock): + get_ssh_fingerprints_mock.return_value = {} + + with client as request: + response = request.get("/") + + assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode() + + +def test_homepage_stats(redis, packages): + with client as request: + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + + 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+') + ] + + 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') + assert key.text.strip() == expected_key + assert re.match(expected_regex, value.text.strip()) + + +def test_homepage_updates(redis, packages): + with client as request: + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + # Run the request a second time to exercise the Redis path. + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + # We expect to see the latest 15 packages, which happens to be + # pkg_49 .. pkg_34. So, create a list of expectations using a range + # starting at 49, stepping down to 49 - 15, -1 step at a time. + 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) + assert pkgname.text.strip() == expected + + +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) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + response = request.get("/", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + # Assert some expectations that we end up getting all fifty + # packages in the "My Packages" table. + 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 + + # 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 + + +def test_homepage_dashboard_requests(redis, packages, user): + now = time.utcnow() + + 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) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + response = request.get("/", cookies=cookies) + 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) + assert pkgname.text.strip() == pkgreq.PackageBaseName + + +def test_homepage_dashboard_flagged_packages(redis, packages, user): + # Set the first Package flagged by setting its OutOfDateTS column. + pkg = packages[0] + with db.begin(): + pkg.PackageBase.OutOfDateTS = time.utcnow() + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + response = request.get("/", cookies=cookies) + 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) + assert flagged_name.text.strip() == pkg.Name diff --git a/test/test_html.py b/test/test_html.py new file mode 100644 index 00000000..5e9505dc --- /dev/null +++ b/test/test_html.py @@ -0,0 +1,167 @@ +""" A test suite used to test HTML renders in different cases. """ +from http import HTTPStatus + +import fastapi +import pytest + +from fastapi import HTTPException +from fastapi.testclient import TestClient + +from aurweb import asgi, db +from aurweb.models import PackageBase +from aurweb.models.account_type import TRUSTED_USER_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 + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) + + +@pytest.fixture +def user() -> User: + with db.begin(): + 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: + with db.begin(): + user.AccountTypeID = TRUSTED_USER_ID + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=user) + yield pkgbase + + +def test_archdev_navbar(client: TestClient): + expected = [ + "AUR Home", + "Packages", + "Register", + "Login" + ] + with client as request: + resp = request.get("/") + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + items = root.xpath('//div[@id="archdev-navbar"]/ul/li/a') + for i, item in enumerate(items): + assert item.text.strip() == expected[i] + + +def test_archdev_navbar_authenticated(client: TestClient, user: User): + expected = [ + "Dashboard", + "Packages", + "Requests", + "My Account", + "Logout" + ] + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/", cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + items = root.xpath('//div[@id="archdev-navbar"]/ul/li/a') + for i, item in enumerate(items): + assert item.text.strip() == expected[i] + + +def test_archdev_navbar_authenticated_tu(client: TestClient, + trusted_user: User): + expected = [ + "Dashboard", + "Packages", + "Requests", + "Accounts", + "My Account", + "Trusted User", + "Logout" + ] + cookies = {"AURSID": trusted_user.login(Request(), "testPassword")} + with client as request: + resp = request.get("/", cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + items = root.xpath('//div[@id="archdev-navbar"]/ul/li/a') + for i, item in enumerate(items): + assert item.text.strip() == expected[i] + + +def test_get_errors(): + html = """ +
    +
  • Test
  • +
+""" + errors = get_errors(html) + assert errors[0].text.strip() == "Test" + + +def test_get_successes(): + html = """ +
    +
  • Test
  • +
+""" + successes = get_successes(html) + assert successes[0].text.strip() == "Test" + + +def test_metrics(client: TestClient): + with client as request: + resp = request.get("/metrics") + assert resp.status_code == int(HTTPStatus.OK) + assert resp.headers.get("Content-Type").startswith("text/plain") + + +def test_404_with_valid_pkgbase(client: TestClient, pkgbase: PackageBase): + """ Test HTTPException with status_code == 404 and valid pkgbase. """ + endpoint = f"/{pkgbase.Name}" + with client as request: + response = request.get(endpoint) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + body = response.text + assert "404 - Page Not Found" in body + assert "To clone the Git repository" in body + + +def test_404(client: TestClient): + """ 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) + + 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). """ + @asgi.app.get("/raise-503") + async def raise_503(request: fastapi.Request): + raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE) + + with TestClient(app=asgi.app) as request: + response = request.get("/raise-503") + assert response.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) diff --git a/test/test_initdb.py b/test/test_initdb.py new file mode 100644 index 00000000..44681d8e --- /dev/null +++ b/test/test_initdb.py @@ -0,0 +1,29 @@ +import pytest + +import aurweb.config +import aurweb.db +import aurweb.initdb + +from aurweb.models.account_type import AccountType + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +class Args: + use_alembic = True + verbose = True + + +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() + assert record is not None diff --git a/test/test_l10n.py b/test/test_l10n.py new file mode 100644 index 00000000..c24c5f55 --- /dev/null +++ b/test/test_l10n.py @@ -0,0 +1,52 @@ +""" Test our l10n module. """ +from aurweb import filters, l10n +from aurweb.testing.requests import Request + + +def test_translator(): + """ 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. """ + request = Request() + assert l10n.get_request_language(request) == "en" + + request.cookies["AURLANG"] = "de" + 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. """ + request = Request() + request.cookies["AURLANG"] = "de" + translator = l10n.get_raw_translator_for_request(request) + 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. """ + request = Request() + request.cookies["AURLANG"] = "de" + + translate = l10n.get_translator_for_request(request) + assert translate("Home") == "Startseite" + + +def test_tn_filter(): + request = Request() + request.cookies["AURLANG"] = "en" + context = {"language": "en", "request": request} + + 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.") + assert translated == "%d packages found." diff --git a/test/test_license.py b/test/test_license.py new file mode 100644 index 00000000..b34bd260 --- /dev/null +++ b/test/test_license.py @@ -0,0 +1,23 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.license import License + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_license_creation(): + with db.begin(): + license = db.create(License, Name="Test License") + assert bool(license.ID) + assert license.Name == "Test License" + + +def test_license_null_name_raises_exception(): + with pytest.raises(IntegrityError): + License() diff --git a/test/test_logging.py b/test/test_logging.py new file mode 100644 index 00000000..63092d07 --- /dev/null +++ b/test/test_logging.py @@ -0,0 +1,16 @@ +from aurweb import logging + +logger = logging.get_logger(__name__) + + +def test_logging(caplog): + logger.info("Test log.") + + # Test that we logged once. + assert len(caplog.records) == 1 + + # Test that our log record was of INFO level. + assert caplog.records[0].levelname == "INFO" + + # Test that our message got logged. + assert "Test log." in caplog.text diff --git a/test/test_mkpkglists.py b/test/test_mkpkglists.py new file mode 100644 index 00000000..ee66e4e1 --- /dev/null +++ b/test/test_mkpkglists.py @@ -0,0 +1,215 @@ +import json + +from typing import List, Union +from unittest import mock + +import pytest + +from aurweb import config, db, util +from aurweb.models import License, Package, PackageBase, PackageDependency, PackageLicense, User +from aurweb.models.account_type import USER_ID +from aurweb.models.dependency_type import DEPENDS_ID +from aurweb.testing import noop + + +class FakeFile: + data = str() + __exit__ = noop + + def __init__(self, modes: str) -> "FakeFile": + self.modes = modes + + def __enter__(self, *args, **kwargs) -> "FakeFile": + return self + + def write(self, data: Union[str, bytes]) -> None: + if isinstance(data, bytes): + data = data.decode() + self.data += data + + def writelines(self, dataset: List[Union[str, bytes]]) -> None: + util.apply_all(dataset, self.write) + + def close(self) -> None: + return + + +class MockGzipOpen: + def __init__(self): + self.gzips = dict() + + def open(self, archive: str, modes: str): + self.gzips[archive] = FakeFile(modes) + return self.gzips.get(archive) + + def get(self, key: str) -> FakeFile: + return self.gzips.get(key) + + def __getitem__(self, key: str) -> FakeFile: + return self.get(key) + + def __contains__(self, key: str) -> bool: + return key in self.gzips + + def data(self, archive: str): + return self.get(archive).data + + +@pytest.fixture(autouse=True) +def setup(db_test): + config.rehash() + + +@pytest.fixture +def user() -> User: + with db.begin(): + 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]: + 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}") + + # 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") + + # Add the package to our output list. + output.append(pkg) + + # Sort output by the package name and return it. + yield sorted(output, key=lambda k: k.Name) + + +@mock.patch("os.makedirs", side_effect=noop) +def test_mkpkglists_empty(makedirs: mock.MagicMock): + gzips = MockGzipOpen() + with mock.patch("gzip.open", side_effect=gzips.open): + from aurweb.scripts import mkpkglists + mkpkglists.main() + + archives = config.get_section("mkpkglists") + archives.pop("archivedir") + archives.pop("packagesmetaextfile") + + for archive in archives.values(): + assert archive in gzips + + # Expect that packagesfile got created, but is empty because + # we have no DB records. + packages_file = archives.get("packagesfile") + assert gzips.data(packages_file) == str() + + # Expect that pkgbasefile got created, but is empty because + # we have no DB records. + users_file = archives.get("pkgbasefile") + assert gzips.data(users_file) == str() + + # Expect that userfile got created, but is empty because + # we have no DB records. + users_file = archives.get("userfile") + assert gzips.data(users_file) == str() + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetafile") + assert gzips.data(meta_file) == "[\n]" + + +@mock.patch("sys.argv", ["mkpkglists", "--extended"]) +@mock.patch("os.makedirs", side_effect=noop) +def test_mkpkglists_extended_empty(makedirs: mock.MagicMock): + gzips = MockGzipOpen() + with mock.patch("gzip.open", side_effect=gzips.open): + from aurweb.scripts import mkpkglists + mkpkglists.main() + + archives = config.get_section("mkpkglists") + archives.pop("archivedir") + + for archive in archives.values(): + assert archive in gzips + + # Expect that packagesfile got created, but is empty because + # we have no DB records. + packages_file = archives.get("packagesfile") + assert gzips.data(packages_file) == str() + + # Expect that pkgbasefile got created, but is empty because + # we have no DB records. + users_file = archives.get("pkgbasefile") + assert gzips.data(users_file) == str() + + # Expect that userfile got created, but is empty because + # we have no DB records. + users_file = archives.get("userfile") + assert gzips.data(users_file) == str() + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetafile") + assert gzips.data(meta_file) == "[\n]" + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetaextfile") + assert gzips.data(meta_file) == "[\n]" + + +@mock.patch("sys.argv", ["mkpkglists", "--extended"]) +@mock.patch("os.makedirs", side_effect=noop) +def test_mkpkglists_extended(makedirs: mock.MagicMock, user: User, + packages: List[Package]): + gzips = MockGzipOpen() + with mock.patch("gzip.open", side_effect=gzips.open): + from aurweb.scripts import mkpkglists + mkpkglists.main() + + archives = config.get_section("mkpkglists") + archives.pop("archivedir") + + for archive in archives.values(): + assert archive in gzips + + # Expect that packagesfile got created, but is empty because + # we have no DB records. + packages_file = archives.get("packagesfile") + expected = "\n".join([p.Name for p in packages]) + "\n" + assert gzips.data(packages_file) == expected + + # Expect that pkgbasefile got created, but is empty because + # we have no DB records. + users_file = archives.get("pkgbasefile") + expected = "\n".join([p.PackageBase.Name for p in packages]) + "\n" + assert gzips.data(users_file) == expected + + # Expect that userfile got created, but is empty because + # we have no DB records. + users_file = archives.get("userfile") + assert gzips.data(users_file) == "test\n" + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetafile") + data = json.loads(gzips.data(meta_file)) + assert len(data) == 5 + + # Expect that packagesmetafile got created, but is empty because + # we have no DB records; it's still a valid empty JSON list. + meta_file = archives.get("packagesmetaextfile") + data = json.loads(gzips.data(meta_file)) + assert len(data) == 5 diff --git a/test/test_notify.py b/test/test_notify.py new file mode 100644 index 00000000..a8e994c5 --- /dev/null +++ b/test/test_notify.py @@ -0,0 +1,665 @@ +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.request_type import ORPHAN_ID +from aurweb.scripts import notify, rendercomment +from aurweb.testing.email import Email +from aurweb.testing.smtp import FakeSMTP, FakeSMTP_SSL + +aur_location = config.get("options", "aur_location") +aur_request_ml = config.get("options", "aur_request_ml") + + +@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", + 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) + 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) + yield user2 + + +@pytest.fixture +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) + yield output + + +@pytest.fixture +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()) + yield pkgreq_ + + +@pytest.fixture +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")) + yield output + + +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) + + # Send the notification for pkgbases[0]. + notif = notify.FlagNotification(user.ID, pkgbases[0].ID) + notif.send() + + # Should've gotten three emails: maintainer + the two comaintainers. + assert Email.count() == 3 + + # Comaintainer 1. + first = Email(1).parse() + assert first.headers.get("To") == user1.Email + + expected = f"AUR Out-of-date Notification for {pkgbase.Name}" + assert first.headers.get("Subject") == expected + + # Comaintainer 2. + second = Email(2).parse() + assert second.headers.get("To") == user2.Email + + # Maintainer. + third = Email(3).parse() + assert third.headers.get("To") == user.Email + + +def test_reset(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + notif = notify.ResetKeyNotification(user.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + expected = "AUR Password Reset" + assert email.headers.get("Subject") == expected + + expected = f"""\ +A password reset request was submitted for the account test 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. + +[1] {aur_location}/passreset/?resetkey=12345678901234567890123456789012\ +""" + assert email.body == expected + + +def test_welcome(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + notif = notify.WelcomeNotification(user.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + expected = "Welcome to the Arch User Repository" + assert email.headers.get("Subject") == expected + + expected = f"""\ +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. + +[1] {aur_location}/passreset/?resetkey=12345678901234567890123456789012\ +""" + assert email.body == expected + + +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.") + rendercomment.update_comment_render_fastapi(comment) + + notif = notify.CommentNotification(user2.ID, pkgbase.ID, comment.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Comment for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] added the following comment to {pkgbase.Name} [2]: + +This is a test comment. + +-- +If you no longer wish to receive notifications about this package, +please go to the package page [2] and select "Disable notifications". + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert expected == email.body + + +def test_update(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + with db.begin(): + user.UpdateNotify = 1 + + notif = notify.UpdateNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Package Update: {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] pushed a new commit to {pkgbase.Name} [2]. + +-- +If you no longer wish to receive notifications about this package, +please go to the package page [2] and select "Disable notifications". + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert expected == email.body + + +def test_adopt(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.AdoptNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Ownership Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +The package {pkgbase.Name} [1] was adopted by {user2.Username} [2]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/ +[2] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + +def test_disown(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.DisownNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Ownership Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +The package {pkgbase.Name} [1] was disowned by {user2.Username} [2]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/ +[2] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + +def test_comaintainer_addition(user: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Co-Maintainer Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +You were added to the co-maintainer list of {pkgbase.Name} [1]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + +def test_comaintainer_removal(user: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.ComaintainerRemoveNotification(user.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Co-Maintainer Notification for {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +You were removed from the co-maintainer list of {pkgbase.Name} [1]. + +[1] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + +def test_delete(user: User, user2: User, pkgbases: List[PackageBase]): + pkgbase = pkgbases[0] + notif = notify.DeleteNotification(user2.ID, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Package deleted: {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] deleted {pkgbase.Name} [2]. + +You will no longer receive notifications about this package. + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + +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() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"AUR Package deleted: {source.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] merged {source.Name} [2] into {target.Name} [3]. + +-- +If you no longer wish receive notifications about the new package, +please go to [3] and click "Disable notifications". + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{source.Name}/ +[3] {aur_location}/pkgbase/{target.Name}/\ +""" + assert email.body == expected + + +def set_tu(users: List[User]) -> User: + with db.begin(): + for user in users: + user.AccountTypeID = TRUSTED_USER_ID + + +def test_open_close_request(user: User, user2: User, + pkgreq: PackageRequest, + pkgbases: List[PackageBase]): + set_tu([user]) + pkgbase = pkgbases[0] + + # Send an open request notification. + notif = notify.RequestOpenNotification( + user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID) + notif.send() + assert Email.count() == 1 + + email = Email(1).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 {pkgbase.Name}" + assert email.headers.get("Subject") == expected + + expected = f"""\ +{user2.Username} [1] filed an orphan request for {pkgbase.Name} [2]: + +This is a request test comment. + +[1] {aur_location}/account/{user2.Username}/ +[2] {aur_location}/pkgbase/{pkgbase.Name}/\ +""" + assert email.body == expected + + # Now send a closure notification on the pkgbase we just opened. + notif = notify.RequestCloseNotification(user2.ID, pkgreq.ID, "rejected") + notif.send() + assert Email.count() == 2 + + email = Email(2).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 {pkgbase.Name} Rejected" + assert email.headers.get("Subject") == expected + + expected = f"""\ +Request #{pkgreq.ID} has been rejected by {user2.Username} [1]. + +[1] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + # Test auto-accept. + notif = notify.RequestCloseNotification(0, pkgreq.ID, "accepted") + notif.send() + assert Email.count() == 3 + + 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") + assert email.headers.get("Subject") == expected + + 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]): + pkgbase = pkgbases[0] + with db.begin(): + db.create(models.PackageComaintainer, PackageBase=pkgbase, + User=user2, Priority=1) + + notif = notify.RequestCloseNotification(0, pkgreq.ID, "accepted") + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == aur_request_ml + 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]): + pkgbase = pkgbases[0] + with db.begin(): + pkgreq.ClosureComment = "This is a test closure comment." + + notif = notify.RequestCloseNotification(user2.ID, pkgreq.ID, "accepted") + notif.send() + assert Email.count() == 1 + + email = Email(1).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 {pkgbase.Name} Accepted" + assert email.headers.get("Subject") == expected + + expected = f"""\ +Request #{pkgreq.ID} has been accepted by {user2.Username} [1]: + +This is a test closure comment. + +[1] {aur_location}/account/{user2.Username}/\ +""" + assert email.body == expected + + +def test_tu_vote_reminders(user: User): + set_tu([user]) + + vote_id = 1 + notif = notify.TUVoteReminderNotification(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}" + 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}\ +""" + assert email.body == expected + + +def test_notify_main(user: User): + """ Test TU vote reminder through aurweb.notify.main(). """ + set_tu([user]) + + vote_id = 1 + args = ["aurweb-notify", "tu-vote-reminder", str(vote_id)] + with mock.patch("sys.argv", args): + notify.main() + + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + expected = f"TU 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}\ +""" + assert email.body == expected + + +# Save original config.get; we're going to mock it and need +# to be able to fallback when we are not overriding. +config_get = config.get + + +def mock_smtp_config(cls): + def _mock_smtp_config(section: str, key: str): + if section == "notifications": + if key == "sendmail": + return cls() + elif key == "smtp-use-ssl": + return cls(0) + elif key == "smtp-use-starttls": + return cls(0) + elif key == "smtp-user": + return cls() + elif key == "smtp-password": + return cls() + return cls(config_get(section, key)) + return _mock_smtp_config + + +def test_smtp(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + SMTP = FakeSMTP() + + get = "aurweb.config.get" + getboolean = "aurweb.config.getboolean" + with mock.patch(get, side_effect=mock_smtp_config(str)): + with mock.patch(getboolean, side_effect=mock_smtp_config(bool)): + with mock.patch("smtplib.SMTP", side_effect=lambda a, b: SMTP): + config.rehash() + notif = notify.WelcomeNotification(user.ID) + notif.send() + config.rehash() + assert len(SMTP.emails) == 1 + + +def mock_smtp_starttls_config(cls): + def _mock_smtp_starttls_config(section: str, key: str): + if section == "notifications": + if key == "sendmail": + return cls() + elif key == "smtp-use-ssl": + return cls(0) + elif key == "smtp-use-starttls": + return cls(1) + elif key == "smtp-user": + return cls("test") + elif key == "smtp-password": + return cls("password") + return cls(config_get(section, key)) + return _mock_smtp_starttls_config + + +def test_smtp_starttls(user: User): + # This test does two things: test starttls path and test + # path where we have a backup email. + + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + user.BackupEmail = "backup@example.org" + + SMTP = FakeSMTP() + + 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("smtplib.SMTP", side_effect=lambda a, b: SMTP): + notif = notify.WelcomeNotification(user.ID) + notif.send() + assert SMTP.starttls_enabled + assert SMTP.user + assert SMTP.passwd + + assert len(SMTP.emails) == 2 + to = SMTP.emails[0][1] + assert to == [user.Email] + + to = SMTP.emails[1][1] + assert to == [user.BackupEmail] + + +def mock_smtp_ssl_config(cls): + def _mock_smtp_ssl_config(section: str, key: str): + if section == "notifications": + if key == "sendmail": + return cls() + elif key == "smtp-use-ssl": + return cls(1) + elif key == "smtp-use-starttls": + return cls(0) + elif key == "smtp-user": + return cls("test") + elif key == "smtp-password": + return cls("password") + return cls(config_get(section, key)) + return _mock_smtp_ssl_config + + +def test_smtp_ssl(user: User): + with db.begin(): + user.ResetKey = "12345678901234567890123456789012" + + SMTP = FakeSMTP_SSL() + + get = "aurweb.config.get" + getboolean = "aurweb.config.getboolean" + with mock.patch(get, side_effect=mock_smtp_ssl_config(str)): + with mock.patch(getboolean, side_effect=mock_smtp_ssl_config(bool)): + with mock.patch("smtplib.SMTP_SSL", side_effect=lambda a, b: SMTP): + notif = notify.WelcomeNotification(user.ID) + notif.send() + assert len(SMTP.emails) == 1 + assert SMTP.use_ssl + assert SMTP.user + assert SMTP.passwd + + +def test_notification_defaults(): + notif = notify.Notification() + assert notif.get_refs() == tuple() + assert notif.get_headers() == dict() + assert notif.get_cc() == list() + + +def test_notification_oserror(user: User, caplog: pytest.LogCaptureFixture): + """ Try sending a notification with a bad SMTP configuration. """ + caplog.set_level(ERROR) + config_get = config.get + + mocked_options = { + "sendmail": str(), + "smtp-server": "mail.server.xyz", + "smtp-port": "587", + "smtp-user": "notify@server.xyz", + "smtp-password": "notify_server_xyz", + "sender": "notify@server.xyz", + "reply-to": "no-reply@server.xyz" + } + + def mock_config_get(section: str, key: str) -> str: + if section == "notifications": + if key in mocked_options: + return mocked_options.get(key) + return config_get(section, key) + + notif = notify.WelcomeNotification(user.ID) + with mock.patch("aurweb.config.get", side_effect=mock_config_get): + notif.send() + + expected = "Unable to emit notification due to an OSError" + assert expected in caplog.text diff --git a/test/test_official_provider.py b/test/test_official_provider.py new file mode 100644 index 00000000..9287ea2d --- /dev/null +++ b/test/test_official_provider.py @@ -0,0 +1,65 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.official_provider import OfficialProvider + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_official_provider_creation(): + with db.begin(): + 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" + assert oprovider.Provides == "some-provides" + + +def test_official_provider_cs(): + """ Test case sensitivity of the database table. """ + with db.begin(): + 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") + assert bool(oprovider_cs.ID) + + assert oprovider.ID != oprovider_cs.ID + + assert oprovider.Name == "some-name" + assert oprovider.Repo == "some-repo" + assert oprovider.Provides == "some-provides" + + assert oprovider_cs.Name == "SOME-NAME" + assert oprovider_cs.Repo == "SOME-REPO" + assert oprovider_cs.Provides == "SOME-PROVIDES" + + +def test_official_provider_null_name_raises_exception(): + with pytest.raises(IntegrityError): + OfficialProvider(Repo="some-repo", Provides="some-provides") + + +def test_official_provider_null_repo_raises_exception(): + with pytest.raises(IntegrityError): + OfficialProvider(Name="some-name", Provides="some-provides") + + +def test_official_provider_null_provides_raises_exception(): + with pytest.raises(IntegrityError): + OfficialProvider(Name="some-name", Repo="some-repo") diff --git a/test/test_package.py b/test/test_package.py new file mode 100644 index 00000000..1408a182 --- /dev/null +++ b/test/test_package.py @@ -0,0 +1,68 @@ +import pytest + +from sqlalchemy import and_ +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +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): + 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 + + +@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") + yield package + + +def test_package(package: Package): + assert package.Name == "beautiful-package" + assert package.Description == "Test description." + assert package.Version == str() # Default version. + assert package.URL == "https://test.package" + + # Update package Version. + with db.begin(): + 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() + 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") + + +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") diff --git a/test/test_package_base.py b/test/test_package_base.py new file mode 100644 index 00000000..5be7e40b --- /dev/null +++ b/test/test_package_base.py @@ -0,0 +1,67 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.package_base import PackageBase +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=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + pkgbase = db.create(PackageBase, Name="beautiful-package", + Maintainer=user) + yield pkgbase + + +def test_package_base(user: User, pkgbase: PackageBase): + assert pkgbase in user.maintained_bases + assert not pkgbase.OutOfDateTS + assert pkgbase.SubmittedTS > 0 + assert pkgbase.ModifiedTS > 0 + + # Set Popularity to a string, then get it by attribute to + # exercise the string -> float conversion path. + with db.begin(): + pkgbase.Popularity = "0.0" + assert pkgbase.Popularity == 0.0 + + +def test_package_base_ci(user: User, pkgbase: PackageBase): + """ Test case insensitivity of the database table. """ + with pytest.raises(IntegrityError): + with db.begin(): + db.create(PackageBase, Name=pkgbase.Name.upper(), Maintainer=user) + db.rollback() + + +def test_package_base_relationships(user: User, pkgbase: PackageBase): + with db.begin(): + pkgbase.Flagger = user + pkgbase.Submitter = user + pkgbase.Packager = user + assert pkgbase in user.flagged_bases + assert pkgbase in user.maintained_bases + assert pkgbase in user.submitted_bases + assert pkgbase in user.package_bases + + +def test_package_base_null_name_raises_exception(): + with pytest.raises(IntegrityError): + PackageBase() diff --git a/test/test_package_blacklist.py b/test/test_package_blacklist.py new file mode 100644 index 00000000..427c3be4 --- /dev/null +++ b/test/test_package_blacklist.py @@ -0,0 +1,23 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.package_blacklist import PackageBlacklist + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_package_blacklist_creation(): + with db.begin(): + package_blacklist = db.create(PackageBlacklist, Name="evil-package") + assert bool(package_blacklist.ID) + assert package_blacklist.Name == "evil-package" + + +def test_package_blacklist_null_name_raises_exception(): + with pytest.raises(IntegrityError): + PackageBlacklist() diff --git a/test/test_package_comaintainer.py b/test/test_package_comaintainer.py new file mode 100644 index 00000000..e377edc0 --- /dev/null +++ b/test/test_package_comaintainer.py @@ -0,0 +1,56 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer +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=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase + + +def test_package_comaintainer_creation(user: User, pkgbase: PackageBase): + with db.begin(): + 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 + assert package_comaintainer.Priority == 5 + + +def test_package_comaintainer_null_user_raises(pkgbase: PackageBase): + with pytest.raises(IntegrityError): + PackageComaintainer(PackageBase=pkgbase, Priority=1) + + +def test_package_comaintainer_null_pkgbase_raises(user: User): + with pytest.raises(IntegrityError): + PackageComaintainer(User=user, Priority=1) + + +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 new file mode 100644 index 00000000..c89e23af --- /dev/null +++ b/test/test_package_comment.py @@ -0,0 +1,66 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comment import PackageComment +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=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase + + +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.") + 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.") + + +def test_package_comment_null_user_raises(pkgbase: PackageBase): + with pytest.raises(IntegrityError): + PackageComment(PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment="Test rendered comment.") + + +def test_package_comment_null_comments_raises(user: User, + pkgbase: PackageBase): + with pytest.raises(IntegrityError): + PackageComment(PackageBase=pkgbase, User=user, + RenderedComment="Test rendered comment.") + + +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.") + assert record.RenderedComment == str() diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py new file mode 100644 index 00000000..2afbc1e3 --- /dev/null +++ b/test/test_package_dependency.py @@ -0,0 +1,66 @@ +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.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_dependency import PackageDependency +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=str(), + AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +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") + 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") + assert pkgdep.DepName == "test-dep" + assert pkgdep.Package == package + assert pkgdep in package.package_dependencies + assert not pkgdep.is_package() + + with db.begin(): + base = db.create(PackageBase, Name=pkgdep.DepName, Maintainer=user) + db.create(Package, PackageBase=base, Name=pkgdep.DepName) + + assert pkgdep.is_package() + + +def test_package_dependencies_null_package_raises(): + with pytest.raises(IntegrityError): + PackageDependency(DepTypeID=DEPENDS_ID, DepName="test-dep") + + +def test_package_dependencies_null_dependency_type_raises(package: Package): + with pytest.raises(IntegrityError): + PackageDependency(Package=package, DepName="test-dep") + + +def test_package_dependencies_null_depname_raises(package: Package): + with pytest.raises(IntegrityError): + PackageDependency(DepTypeID=DEPENDS_ID, Package=package) diff --git a/test/test_package_group.py b/test/test_package_group.py new file mode 100644 index 00000000..0cb83ee2 --- /dev/null +++ b/test/test_package_group.py @@ -0,0 +1,57 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.group import Group +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_group import PackageGroup +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=USER_ID) + yield user + + +@pytest.fixture +def group() -> Group: + with db.begin(): + group = db.create(Group, Name="Test Group") + yield group + + +@pytest.fixture +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) + yield package + + +def test_package_group(package: Package, group: Group): + with db.begin(): + package_group = db.create(PackageGroup, Package=package, Group=group) + assert package_group.Group == group + assert package_group.Package == package + + +def test_package_group_null_package_raises(group: Group): + with pytest.raises(IntegrityError): + PackageGroup(Group=group) + + +def test_package_group_null_group_raises(package: Package): + with pytest.raises(IntegrityError): + PackageGroup(Package=package) diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py new file mode 100644 index 00000000..ff466efc --- /dev/null +++ b/test/test_package_keyword.py @@ -0,0 +1,44 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.package_base import PackageBase +from aurweb.models.package_keyword import PackageKeyword +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=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + 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") + assert pkg_keyword in pkgbase.keywords + assert pkgbase == pkg_keyword.PackageBase + + +def test_package_keyword_null_pkgbase_raises_exception(): + with pytest.raises(IntegrityError): + PackageKeyword(Keyword="test") diff --git a/test/test_package_license.py b/test/test_package_license.py new file mode 100644 index 00000000..c43423b8 --- /dev/null +++ b/test/test_package_license.py @@ -0,0 +1,58 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.license import License +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_license import PackageLicense +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=USER_ID) + yield user + + +@pytest.fixture +def license() -> License: + with db.begin(): + license = db.create(License, Name="Test License") + yield license + + +@pytest.fixture +def package(user: User, license: License): + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + yield package + + +def test_package_license(license: License, package: Package): + with db.begin(): + package_license = db.create(PackageLicense, Package=package, + License=license) + assert package_license.License == license + assert package_license.Package == package + + +def test_package_license_null_package_raises(license: License): + with pytest.raises(IntegrityError): + PackageLicense(License=license) + + +def test_package_license_null_license_raises(package: Package): + with pytest.raises(IntegrityError): + PackageLicense(Package=package) diff --git a/test/test_package_notification.py b/test/test_package_notification.py new file mode 100644 index 00000000..e7a72a43 --- /dev/null +++ b/test/test_package_notification.py @@ -0,0 +1,47 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.package_base import PackageBase +from aurweb.models.package_notification import PackageNotification +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") + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase + + +def test_package_notification_creation(user: User, pkgbase: PackageBase): + with db.begin(): + package_notification = db.create( + PackageNotification, User=user, PackageBase=pkgbase) + assert bool(package_notification) + assert package_notification.User == user + assert package_notification.PackageBase == pkgbase + + +def test_package_notification_null_user_raises(pkgbase: PackageBase): + with pytest.raises(IntegrityError): + PackageNotification(PackageBase=pkgbase) + + +def test_package_notification_null_pkgbase_raises(user: User): + with pytest.raises(IntegrityError): + PackageNotification(User=user) diff --git a/test/test_package_relation.py b/test/test_package_relation.py new file mode 100644 index 00000000..6e9a5545 --- /dev/null +++ b/test/test_package_relation.py @@ -0,0 +1,67 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_relation import PackageRelation +from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID +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=USER_ID) + yield user + + +@pytest.fixture +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") + yield package + + +def test_package_relation(package: Package): + with db.begin(): + pkgrel = db.create(PackageRelation, Package=package, + RelTypeID=CONFLICTS_ID, + RelName="test-relation") + + assert pkgrel.RelName == "test-relation" + assert pkgrel.Package == package + assert pkgrel in package.package_relations + + with db.begin(): + pkgrel.RelTypeID = PROVIDES_ID + + with db.begin(): + pkgrel.RelTypeID = REPLACES_ID + + +def test_package_relation_null_package_raises(): + with pytest.raises(IntegrityError): + PackageRelation(RelTypeID=CONFLICTS_ID, RelName="test-relation") + + +def test_package_relation_null_relation_type_raises(package: Package): + with pytest.raises(IntegrityError): + PackageRelation(Package=package, RelName="test-relation") + + +def test_package_relation_null_relname_raises(package: Package): + with pytest.raises(IntegrityError): + PackageRelation(Package=package, RelTypeID=CONFLICTS_ID) diff --git a/test/test_package_request.py b/test/test_package_request.py new file mode 100644 index 00000000..3474c565 --- /dev/null +++ b/test/test_package_request.py @@ -0,0 +1,142 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import 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.request_type import MERGE_ID +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=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase + + +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()) + + assert bool(package_request.ID) + assert package_request.User == user + assert package_request.PackageBase == pkgbase + assert package_request.PackageBaseName == pkgbase.Name + assert package_request.Comments == str() + assert package_request.ClosureComment == str() + + # Make sure that everything is cross-referenced with relationships. + assert package_request in user.package_requests + assert package_request in pkgbase.requests + + +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()) + + assert package_request.Closer == user + assert package_request.ClosedTS == ts + + # Test relationships. + assert package_request in user.closed_requests + + +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()) + + +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()) + + +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()) + + +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()) + + +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()) + + +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()) + + +def test_package_request_status_display(user: User, pkgbase: PackageBase): + """ 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) + assert pkgreq.status_display() == PENDING + + with db.begin(): + pkgreq.Status = CLOSED_ID + assert pkgreq.status_display() == CLOSED + + with db.begin(): + pkgreq.Status = ACCEPTED_ID + assert pkgreq.status_display() == ACCEPTED + + with db.begin(): + pkgreq.Status = REJECTED_ID + assert pkgreq.status_display() == REJECTED + + with db.begin(): + pkgreq.Status = 124 + with pytest.raises(KeyError): + pkgreq.status_display() diff --git a/test/test_package_source.py b/test/test_package_source.py new file mode 100644 index 00000000..e5797f90 --- /dev/null +++ b/test/test_package_source.py @@ -0,0 +1,46 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_source import PackageSource +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=USER_ID) + yield user + + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + package = db.create(Package, PackageBase=pkgbase, Name="test-package") + yield package + + +def test_package_source(package: Package): + with db.begin(): + pkgsource = db.create(PackageSource, Package=package) + assert pkgsource.Package == package + # By default, PackageSources.Source assigns the string '/dev/null'. + assert pkgsource.Source == "/dev/null" + assert pkgsource.SourceArch is None + + +def test_package_source_null_package_raises(): + with pytest.raises(IntegrityError): + PackageSource() diff --git a/test/test_package_vote.py b/test/test_package_vote.py new file mode 100644 index 00000000..24d2fdd2 --- /dev/null +++ b/test/test_package_vote.py @@ -0,0 +1,57 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db, time +from aurweb.models.account_type import USER_ID +from aurweb.models.package_base import PackageBase +from aurweb.models.package_vote import PackageVote +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=str(), + AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) + yield pkgbase + + +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) + assert bool(package_vote) + assert package_vote.User == user + assert package_vote.PackageBase == pkgbase + assert package_vote.VoteTS == ts + + +def test_package_vote_null_user_raises(pkgbase: PackageBase): + with pytest.raises(IntegrityError): + PackageVote(PackageBase=pkgbase, VoteTS=1) + + +def test_package_vote_null_pkgbase_raises(user: User): + with pytest.raises(IntegrityError): + PackageVote(User=user, VoteTS=1) + + +def test_package_vote_null_votets_raises(user: User, pkgbase: PackageBase): + with pytest.raises(IntegrityError): + PackageVote(User=user, PackageBase=pkgbase) diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py new file mode 100644 index 00000000..adafe1ae --- /dev/null +++ b/test/test_packages_routes.py @@ -0,0 +1,1485 @@ +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.models import License, PackageLicense +from aurweb.models.account_type import USER_ID, AccountType +from aurweb.models.dependency_type import DependencyType +from aurweb.models.official_provider import OfficialProvider +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_comment import PackageComment +from aurweb.models.package_dependency import PackageDependency +from aurweb.models.package_keyword import PackageKeyword +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.request_type import DELETION_ID, RequestType +from aurweb.models.user import User +from aurweb.testing.html import get_errors, get_successes, parse_root +from aurweb.testing.requests import Request + + +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) + 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_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) +def setup(db_test): + return + + +@pytest.fixture +def client() -> TestClient: + """ Yield a FastAPI TestClient. """ + yield TestClient(app=asgi.app) + + +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) + return user + + +@pytest.fixture +def user() -> User: + """ Yield a user. """ + user = create_user("test") + yield user + + +@pytest.fixture +def maintainer() -> User: + """ 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) + yield maintainer + + +@pytest.fixture +def tu_user(): + tu_type = db.query(AccountType, + AccountType.AccountType == "Trusted User").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 + + +@pytest.fixture +def package(maintainer: User) -> Package: + """ 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) + yield package + + +@pytest.fixture +def pkgbase(package: Package) -> PackageBase: + yield package.PackageBase + + +@pytest.fixture +def target(maintainer: User) -> PackageBase: + """ Merge target. """ + now = time.utcnow() + with db.begin(): + 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`. """ + 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()) + yield pkgreq + + +@pytest.fixture +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) + 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}") + packages_.append(package) + + yield packages_ + + +def test_package_not_found(client: TestClient): + with client as request: + resp = request.get("/packages/not_found") + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_package_official_not_found(client: TestClient, package: Package): + """ When a Package has a matching OfficialProvider record, it is not + hosted on AUR, but in the official repositories. Getting a package + with this kind of record should return a status code 404. """ + with db.begin(): + db.create(OfficialProvider, + Name=package.Name, + Repo="core", + Provides=package.Name) + + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_package(client: TestClient, package: Package): + """ 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=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") + + # Create some licenses. + licenses = [ + db.create(License, Name="test_license1"), + db.create(License, Name="test_license2") + ] + + db.create(PackageLicense, PackageID=package.ID, + License=licenses[0]) + db.create(PackageLicense, PackageID=package.ID, + License=licenses[1]) + + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + h2 = root.find('.//div[@id="pkgdetails"]/h2') + + sections = h2.text.split(":") + assert sections[0] == "Package Details" + + name, version = sections[1].lstrip().split(" ") + assert name == package.Name + version == package.Version + + rows = root.findall('.//table[@id="pkginfo"]//tr') + row = rows[1] # Second row is our target. + + pkgbase = row.find("./td/a") + assert pkgbase.text.strip() == package.PackageBase.Name + + licenses = root.xpath('//tr[@id="licenses"]/td') + expected = ["test_license1", "test_license2"] + assert licenses[0].text.strip() == ", ".join(expected) + + provides = root.xpath('//tr[@id="provides"]/td') + expected = ["test_provider1", "test_provider2"] + assert provides[0].text.strip() == ", ".join(expected) + + replaces = root.xpath('//tr[@id="replaces"]/td') + expected = ["test_replacer1", "test_replacer2"] + assert replaces[0].text.strip() == ", ".join(expected) + + conflicts = root.xpath('//tr[@id="conflicts"]/td') + expected = ["test_conflict1", "test_conflict2"] + assert conflicts[0].text.strip() == ", ".join(expected) + + +def test_package_comments(client: TestClient, user: User, package: Package): + now = (time.utcnow()) + with db.begin(): + comment = db.create(PackageComment, PackageBase=package.PackageBase, + User=user, Comments="Test comment", CommentTS=now) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(package_endpoint(package), cookies=cookies) + 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()') + for i, row in enumerate(expected): + assert comments[i].strip() == row + + +def test_package_requests_display(client: TestClient, user: User, + package: Package, pkgreq: PackageRequest): + # Test that a single request displays "1 pending request". + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + selector = '//div[@id="actionlist"]/ul/li/span[@class="flagged"]' + target = root.xpath(selector)[0] + assert target.text.strip() == "1 pending request" + + 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()) + + # Test that a two requests display "2 pending requests". + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + selector = '//div[@id="actionlist"]/ul/li/span[@class="flagged"]' + target = root.xpath(selector)[0] + 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 + authenticated. Form inputs are presented to maintainers. + 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) + assert resp.status_code == int(HTTPStatus.OK) + + expected = [ + "View PKGBUILD", + "View Changes", + "Download snapshot", + "Search wiki", + "Flag package out-of-date", + "Vote for this package", + "Enable notifications", + "Submit Request" + ] + for expected_text in expected: + assert expected_text 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"]' + target = root.xpath(selector) + assert len(target) == 0 + + +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) + assert resp.status_code == int(HTTPStatus.OK) + + expected = [ + "View PKGBUILD", + "View Changes", + "Download snapshot", + "Search wiki", + "Flag package out-of-date", + "Vote for this package", + "Enable notifications", + "Manage Co-Maintainers", + "Submit Request", + "Disown Package" + ] + for expected_text in expected: + assert expected_text in resp.text + + +def test_package_authenticated_tu(client: TestClient, + tu_user: User, + package: Package): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.get(package_endpoint(package), cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + expected = [ + "View PKGBUILD", + "View Changes", + "Download snapshot", + "Search wiki", + "Flag package out-of-date", + "Vote for this package", + "Enable notifications", + "Manage Co-Maintainers", + "Submit Request", + "Delete Package", + "Merge Package", + "Disown Package" + ] + for expected_text in expected: + assert expected_text in resp.text + + +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) + dep = create_package_dep(package, dep_pkg.Name) + + # 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.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") + + # 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") + + # 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.DepDesc = "Test description." + + 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") + create_package_dep(package, "test-dep-99") + + # Also, create a provider who provides our test-dep-99. + provider = create_package("test-provider", maintainer) + create_package_rel(provider, dep.DepName) + + with client as request: + resp = request.get(package_endpoint(package)) + 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() + )) + root = parse_root(resp.text) + pkgdeps = root.findall('.//ul[@id="pkgdepslist"]/li/a') + for i, expectation in enumerate(expected): + assert pkgdeps[i].text.strip() == expectation.DepName + + # 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] + assert arch.text.strip() == "(make, x86_64)" + + # And let's make sure that the broken package was displayed. + broken_node = root.find('.//ul[@id="pkgdepslist"]/li/span') + assert broken_node.text.strip() == broken_dep.DepName + + +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" + }) + 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()) + assert pager_text == "55 packages found. Page 1 of 2." + + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 # Default per-page + + +def test_packages_empty(client: TestClient): + with client as request: + response = request.get("/packages") + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + results = root.xpath('//div[@id="pkglist-results"]/p') + expected = "No packages matched your search criteria." + assert results[0].text.strip() == expected + + +def test_packages_search_by_name(client: TestClient, packages: List[Package]): + with client as request: + response = request.get("/packages", params={ + "SeB": "n", + "K": "pkg_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 # Default per-page + + +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_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + + # There is no package named exactly 'pkg_', we get 0 results. + assert len(rows) == 0 + + with client as request: + response = request.get("/packages", params={ + "SeB": "N", + "K": "pkg_1" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + + # There's just one package named 'pkg_1', we get 1 result. + assert len(rows) == 1 + + +def test_packages_search_by_pkgbase(client: TestClient, + packages: List[Package]): + with client as request: + response = request.get("/packages", params={ + "SeB": "b", + "K": "pkg_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + +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_" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + with client as request: + response = request.get("/packages", params={ + "SeB": "B", + "K": "pkg_1" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +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" + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + # 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") + + # And request packages with that keyword, we should get 1 result. + with client as request: + 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 + + +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 + }) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + # Search again by maintainer with no keywords given. + # This kind of search returns all orphans instead. + # In this first case, there are no orphan packages; assert that. + with client as request: + response = request.get("/packages", params={"SeB": "m"}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + # Orphan `package`. + with db.begin(): + package.PackageBase.Maintainer = None + + # This time, we should get `package` returned, since it's now an orphan. + with client as request: + response = request.get("/packages", params={"SeB": "m"}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +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 + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 0 + + # Now, we create a comaintainer. + with db.begin(): + 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 + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +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 + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + 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) + + with client as request: + response = request.get("/packages", params={ + "SeB": "M", + "K": user.Username + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +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 + }) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +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" + }) + 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] + + with client as request: + response2 = request.get("/packages", params={ + "SB": "n", # Name + "SO": "d", # Ascending + "PP": "150" + }) + 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] + assert rows == list(reversed(rows2)) + + +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. + }) + 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. + 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. + }) + 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. + assert votes.text.strip() == "1" + + +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 + }) + 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. + assert pop.text.strip() == "0.50" + + +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) + + # 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) + 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. + 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. + 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) + + # 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) + 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. + 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. + 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. """ + + # 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) + 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" + }) + 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. + + 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" + }) + 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. + + assert col.text.strip() == maintainer2.Username + + +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] + with db.begin(): + package.PackageBase.ModifiedTS = now - 1000 + + with client as request: + 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. + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + # 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] + assert col.text.strip() == package.Name + + +def test_packages_flagged(client: TestClient, maintainer: User, + packages: List[Package]): + package = packages[0] + + now = time.utcnow() + + with db.begin(): + package.PackageBase.OutOfDateTS = now + package.PackageBase.Flagger = maintainer + + with client as request: + 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. + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + with client as request: + 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 + # page will have 50 results (55 packages - 1 outdated = 54 not outdated). + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + +def test_packages_orphans(client: TestClient, packages: List[Package]): + package = packages[0] + with db.begin(): + package.PackageBase.Maintainer = None + + with client as request: + response = request.get("/packages", params={"submit": "Orphans"}) + assert response.status_code == int(HTTPStatus.OK) + + # We only have one orphan. Let's make sure that's what is returned. + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +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). """ + with db.begin(): + for i in range(255): + 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. + with client as request: + response = request.get("/packages", params={"PP": 50}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 50 + + # Alright, test the next case, PP of 100. + with client as request: + response = request.get("/packages", params={"PP": 100}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 100 + + # And finally, the last case, a PP of 250. + with client as request: + response = request.get("/packages", params={"PP": 250}) + assert response.status_code == int(HTTPStatus.OK) + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 250 + + +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) + 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."]) + + 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + errors = get_errors(resp.text) + expected = "Some error." + assert errors[0].text.strip() == expected + + +def test_packages_post(client: TestClient, user: User, package: Package): + + async def stub_action(request: Request, **kwargs): + 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) + assert resp.status_code == int(HTTPStatus.OK) + + errors = get_successes(resp.text) + expected = "Some success." + assert errors[0].text.strip() == expected + + +def test_packages_post_unflag(client: TestClient, user: User, + maintainer: User, package: Package): + # Flag `package` as `user`. + now = time.utcnow() + with db.begin(): + package.PackageBase.Flagger = user + package.PackageBase.OutOfDateTS = now + + cookies = {"AURSID": user.login(Request(), "testPassword")} + + # Don't supply any packages. + post_data = {"action": "unflag", "IDs": []} + with client as request: + resp = request.post("/packages", data=post_data, cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + errors = get_errors(resp.text) + expected = "You did not select any packages to unflag." + assert errors[0].text.strip() == expected + + # 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) + assert resp.status_code == int(HTTPStatus.OK) + assert package.PackageBase.Flagger is None + successes = get_successes(resp.text) + expected = "The selected packages have been unflagged." + assert successes[0].text.strip() == expected + + # Re-flag `package` as `user`. + now = time.utcnow() + with db.begin(): + package.PackageBase.Flagger = user + package.PackageBase.OutOfDateTS = now + + # Try to unflag the package as `maintainer`, which is not allowed. + 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + errors = get_errors(resp.text) + expected = "You did not select any packages to unflag." + assert errors[0].text.strip() == expected + + +def test_packages_post_notify(client: TestClient, user: User, package: Package): + notif = package.PackageBase.notifications.filter( + PackageNotification.UserID == user.ID + ).first() + assert notif is None + + # Try to enable notifications but supply no packages, causing + # an error to be rendered. + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.post("/packages", data={"action": "notify"}, + cookies=cookies) + 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 + + # Now let's actually enable notifications on `package`. + with client as request: + resp = request.post("/packages", data={ + "action": "notify", + "IDs": [package.ID] + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + expected = "The selected packages' notifications have been enabled." + successes = get_successes(resp.text) + assert successes[0].text.strip() == expected + + # 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) + 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): + # Create a notification record. + with db.begin(): + 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + errors = get_errors(resp.text) + expected = "You did not select any packages for notification removal." + assert errors[0].text.strip() == expected + + # Request removal of the notification; really. + with client as request: + resp = request.post("/packages", data={ + "action": "unnotify", + "IDs": [package.ID] + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + successes = get_successes(resp.text) + expected = "The selected packages' notifications have been removed." + assert successes[0].text.strip() == expected + + # Let's ensure the record got removed. + notif = package.PackageBase.notifications.filter( + PackageNotification.UserID == user.ID + ).first() + assert notif is None + + # Try it again. The notif no longer exists. + with client as request: + resp = request.post("/packages", data={ + "action": "unnotify", + "IDs": [package.ID] + }, cookies=cookies) + 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): + + # 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + errors = get_errors(resp.text) + expected = "You did not select any packages to adopt." + assert errors[0].text.strip() == expected + + # 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) + 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." + assert errors[0].text.strip() == expected + + # Remove the maintainer from the DB. + with db.begin(): + package.PackageBase.Maintainer = None + assert package.PackageBase.Maintainer is None + + # Now, let's try to adopt without confirming. + with client as request: + resp = request.post("/packages", data={ + "action": "adopt", + "IDs": [package.ID] + }, cookies=cookies) + 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.") + 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) + 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. """ + # Initially prove that we have a maintainer. + assert package.PackageBase.Maintainer is not None + assert package.PackageBase.Maintainer == maintainer + + # 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + errors = get_errors(resp.text) + expected = "You did not select any packages to disown." + assert errors[0].text.strip() == expected + assert package.PackageBase.Maintainer is not None + + # 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) + 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.") + 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + assert package.PackageBase.Maintainer is not None + errors = get_errors(resp.text) + expected = "You are not allowed to disown one of the packages you selected." + assert errors[0].text.strip() == expected + + # 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) + + assert package.PackageBase.Maintainer is None + successes = get_successes(resp.text) + expected = "The selected packages have been disowned." + 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")} + with client as request: + resp = request.post("/packages", data={ + "action": "disown", + "IDs": [package.ID], + "confirm": True + }, cookies=cookies) + + 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): + # 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + errors = get_errors(resp.text) + expected = "You did not select any packages to delete." + assert errors[0].text.strip() == expected + + # 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) + 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.") + 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) + 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. + # However, this next request will be rejected due to supplying + # an invalid package ID. + tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.post("/packages", data={ + "action": "delete", + "IDs": [0], + "confirm": True + }, cookies=tu_cookies) + 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` + # to delete `package`. + with client as request: + resp = request.post("/packages", data={ + "action": "delete", + "IDs": [package.ID], + "confirm": True + }, cookies=tu_cookies) + assert resp.status_code == int(HTTPStatus.OK) + successes = get_successes(resp.text) + expected = "The selected packages have been deleted." + assert successes[0].text.strip() == expected + + # 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)}.") + 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, + so its being included in the packages routes test suite to + leverage existing fixtures. """ + endpoint = f"/account/{user.Username}/comments" + with client as request: + resp = request.get(endpoint, allow_redirects=False) + 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, + so its being included in the packages routes test suite to + 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) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{user.Username}/comments" + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + comments = root.xpath('//div[@class="article-content"]/div') + + # Assert that we got Comments rendered from the first comment. + 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("

", "") + assert rendered[0].text.strip() == expected diff --git a/test/test_packages_util.py b/test/test_packages_util.py new file mode 100644 index 00000000..02f84601 --- /dev/null +++ b/test/test_packages_util.py @@ -0,0 +1,128 @@ +import pytest + +from fastapi.testclient import TestClient + +from aurweb import asgi, config, db, time +from aurweb.models.account_type import USER_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_notification import PackageNotification +from aurweb.models.package_source import PackageSource +from aurweb.models.package_vote import PackageVote +from aurweb.models.user import User +from aurweb.packages import util +from aurweb.redis import kill_redis + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@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) + yield maintainer + + +@pytest.fixture +def package(maintainer: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-pkg", + Packager=maintainer, Maintainer=maintainer) + package = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) + yield package + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) + + +def test_package_link(client: TestClient, package: Package): + expected = f"/packages/{package.Name}" + assert util.package_link(package) == expected + + +def test_official_package_link(client: TestClient, package: Package): + with db.begin(): + 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 + + +def test_updated_packages(maintainer: User, package: Package): + expected = { + "Name": package.Name, + "Version": package.Version, + "PackageBase": { + "ModifiedTS": package.PackageBase.ModifiedTS + } + } + + kill_redis() # Kill it here to ensure we're on a fake instance. + assert util.updated_packages(1, 0) == [expected] + assert util.updated_packages(1, 600) == [expected] + kill_redis() # Kill it again, in case other tests use a real instance. + + +def test_query_voted(maintainer: User, package: Package): + now = time.utcnow() + with db.begin(): + 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) + assert query_voted[package.PackageBase.ID] + + +def test_query_notified(maintainer: User, package: Package): + with db.begin(): + 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) + assert query_notified[package.PackageBase.ID] + + +def test_source_uri_file(package: Package): + FILE = "test_file" + + with db.begin(): + 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) + + +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") + file, uri = util.source_uri(pkgsrc) + assert (file, uri) == (FILE, URL) + + +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") + file, uri = util.source_uri(pkgsrc) + assert (file, uri) == (URL, URL) diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py new file mode 100644 index 00000000..17f69811 --- /dev/null +++ b/test/test_pkgbase_routes.py @@ -0,0 +1,1333 @@ +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.models.account_type import USER_ID, AccountType +from aurweb.models.dependency_type import DependencyType +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_comment import PackageComment +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_request import ACCEPTED_ID, PackageRequest +from aurweb.models.package_vote import PackageVote +from aurweb.models.relation_type import PROVIDES_ID, RelationType +from aurweb.models.request_type import DELETION_ID, MERGE_ID, RequestType +from aurweb.models.user import User +from aurweb.testing.email import Email +from aurweb.testing.html import get_errors, get_successes, parse_root +from aurweb.testing.requests import Request + + +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) + 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_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) +def setup(db_test): + return + + +@pytest.fixture +def client() -> TestClient: + """ Yield a FastAPI TestClient. """ + yield TestClient(app=asgi.app) + + +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) + return user + + +@pytest.fixture +def user() -> User: + """ Yield a user. """ + user = create_user("test") + yield user + + +@pytest.fixture +def maintainer() -> User: + """ 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) + yield maintainer + + +@pytest.fixture +def tu_user(): + tu_type = db.query(AccountType, + AccountType.AccountType == "Trusted User").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 + + +@pytest.fixture +def package(maintainer: User) -> Package: + """ 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) + yield package + + +@pytest.fixture +def pkgbase(package: Package) -> PackageBase: + yield package.PackageBase + + +@pytest.fixture +def target(maintainer: User) -> PackageBase: + """ Merge target. """ + now = time.utcnow() + with db.begin(): + 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`. """ + 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()) + yield pkgreq + + +@pytest.fixture +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) + 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}") + packages_.append(package) + + yield packages_ + + +@pytest.fixture +def requests(user: User, packages: List[Package]) -> List[PackageRequest]: + pkgreqs = [] + 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()) + pkgreqs.append(pkgreq) + yield pkgreqs + + +def test_pkgbase_not_found(client: TestClient): + with client as request: + resp = request.get("/pkgbase/not_found") + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_redirect(client: TestClient, package: Package): + with client as request: + resp = request.get(f"/pkgbase/{package.Name}", + allow_redirects=False) + 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) + + expected = [package.Name, second.Name] + with client as request: + resp = request.get(f"/pkgbase/{package.Name}", + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + + # Check the details box title. + title = root.find('.//div[@id="pkgdetails"]/h2') + title, pkgname = title.text.split(": ") + assert title == "Package Base Details" + assert pkgname == package.Name + + pkgs = root.findall('.//div[@id="pkgs"]/ul/li/a') + for i, name in enumerate(expected): + assert pkgs[i].text.strip() == name + + +def test_pkgbase_voters(client: TestClient, tu_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) + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + assert resp.status_code == int(HTTPStatus.OK) + + # We should've gotten one link to the voter, tu_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 + + +def test_pkgbase_voters_unauthorized(client: TestClient, user: User, + package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/voters" + + now = time.utcnow() + with db.begin(): + db.create(PackageVote, User=user, PackageBase=pkgbase, VoteTS=now) + + with client as request: + resp = request.get(endpoint, allow_redirects=False) + 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): + 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) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +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) + + 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) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +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) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +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) + 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: + - 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 + """ + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments" + with client as request: + resp = request.post(endpoint, data={ + "comment": "Test comment.", + "enable_notifications": True + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + expected_prefix = f"/pkgbase/{pkgbasename}" + prefix_len = len(expected_prefix) + assert resp.headers.get("location")[:prefix_len] == expected_prefix + + with client as request: + resp = request.get(resp.headers.get("location")) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + headers = root.xpath('//h4[@class="comment-header"]') + bodies = root.xpath('//div[@class="article-content"]/div/p') + + assert len(headers) == 1 + assert len(bodies) == 1 + + assert bodies[0].text.strip() == "Test comment." + comment_id = headers[0].attrib["id"].split("-")[-1] + + # 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) + assert resp.status_code == int(HTTPStatus.OK) + + # Clear up the PackageNotification. This doubles as testing + # that the notification was created and clears it up so we can + # test enabling it during edit. + pkgbase = package.PackageBase + db_notif = pkgbase.notifications.filter( + PackageNotification.UserID == maintainer.ID + ).first() + with db.begin(): + db.delete(db_notif) + + # Now, let's edit the comment we just created. + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + with client as request: + resp = request.get(resp.headers.get("location")) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + headers = root.xpath('//h4[@class="comment-header"]') + bodies = root.xpath('//div[@class="article-content"]/div/p') + + assert len(headers) == 1 + assert len(bodies) == 1 + + assert bodies[0].text.strip() == "Edited comment." + + # 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) + 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) + 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): + # 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + expected = f"/pkgbase/{pkgbasename}" + assert resp.headers.get("location") == expected + + # Test the unauthorized case of comment undeletion. + 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) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + # And move on to undeleting it. + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + +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) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +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) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +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) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +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 + + # Pin the comment. + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Assert that PinnedTS got set. + assert comment.PinnedTS > 0 + + # Unpin the comment we just pinned. + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/unpin" + with client as request: + resp = request.post(endpoint, cookies=cookies) + 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): + 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) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +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) + assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) + + +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) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +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) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +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) + 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): + 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) + 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): + 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) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + assert error.text.strip() == "Invalid user name: fake" + + +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")} + + # 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) + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + # Now that we've added a comaintainer to the pkgbase, + # 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) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + users = root.xpath('//textarea[@id="id_users"]')[0] + assert users.text.strip() == user.Username + + # Finish off by removing all the comaintainers. + with client as request: + resp = request.post(endpoint, data={ + "users": str() + }, cookies=cookies, allow_redirects=False) + 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) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + users = root.xpath('//textarea[@id="id_users"]')[0] + assert users is not None and users.text is None + + +def test_pkgbase_request_not_found(client: TestClient, user: User): + pkgbase_name = "fake" + endpoint = f"/pkgbase/{pkgbase_name}/request" + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_pkgbase_request(client: TestClient, user: User, package: Package): + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/request" + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(endpoint, cookies=cookies) + 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) + assert resp.status_code == int(HTTPStatus.NOT_FOUND) + + +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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + +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) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = "The comment field must not be empty." + 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: + 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) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = "The package base you want to merge into does not exist." + assert error.text.strip() == expected + + +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) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = 'The "Merge into" field must not be empty.' + assert error.text.strip() == expected + + +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) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = "You cannot merge a package base into itself." + assert error.text.strip() == expected + + +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. + assert pkgbase.Flagger is None + + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/flag" + + # Get the flag page. + with client as request: + resp = request.get(endpoint, cookies=cookies) + 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) + 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) + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert pkgbase.Flagger == user + assert pkgbase.FlaggerComment == "Test" + + # 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) + 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) + 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) + + # 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) + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert pkgbase.Flagger is None + + # Flag it again. + with client as request: + resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data={ + "comments": "Test" + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Now, unflag it for real. + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert pkgbase.Flagger is None + + +def test_pkgbase_flag_vcs(client: TestClient, user: User, package: Package): + # Morph our package fixture into a VCS package (-git). + with db.begin(): + package.PackageBase.Name += "-git" + package.Name += "-git" + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag", + cookies=cookies) + 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.") + assert expected in resp.text + + +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() + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + notif = pkgbase.notifications.filter( + PackageNotification.UserID == user.ID + ).first() + assert notif is None + + +def test_pkgbase_vote(client: TestClient, user: User, package: Package): + pkgbase = package.PackageBase + + # We haven't voted yet. + vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() + assert vote is None + + # Vote for the package. + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/vote" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() + assert vote is not None + assert pkgbase.NumVotes == 1 + + # Remove vote. + endpoint = f"/pkgbase/{pkgbase.Name}/unvote" + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() + assert vote is None + assert pkgbase.NumVotes == 0 + + +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) + 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")} + pkgbase = package.PackageBase + endpoint = f"/pkgbase/{pkgbase.Name}/disown" + + with db.begin(): + db.create(PackageComaintainer, + User=user, + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # GET as the maintainer. + with client as request: + resp = request.get(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.OK) + + # POST as a normal user, which is rejected for lack of credentials. + with client as request: + resp = request.post(endpoint, cookies=user_cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # POST as the maintainer without "confirm". + with client as request: + resp = request.post(endpoint, cookies=cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + # POST as the maintainer with "confirm". + with client as request: + resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_pkgbase_adopt(client: TestClient, user: User, tu_user: User, + maintainer: User, package: Package): + # Unset the maintainer as if package is orphaned. + with db.begin(): + package.PackageBase.Maintainer = None + + pkgbasename = package.PackageBase.Name + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbasename}/adopt" + + # Adopt the package base. + with client as request: + resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + 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) + 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")} + with client as request: + resp = request.post(endpoint, cookies=tu_cookies, + allow_redirects=False) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert package.PackageBase.Maintainer == tu_user + + +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) + 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) + 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): + pkgbase = package.PackageBase + + # Test that the GET request works. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/delete" + with client as request: + resp = request.get(endpoint, cookies=cookies) + 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) + 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) + 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() + assert record is None + + # Two emails should've been sent out; an autogenerated + # request's accepted notification and a deletion notification. + assert Email.count() == 1 + + req_close = Email(1).parse() + expr = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$" + subject = req_close.headers.get("Subject") + assert re.match(expr, subject) + + +def test_pkgbase_delete_with_request(client: TestClient, tu_user: User, + pkgbase: PackageBase, + pkgreq: PackageRequest): + # TODO: Test that a previously existing request gets Accepted when + # a TU deleted the package. + + # Delete the package as `tu_user` via POST request. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{pkgbase.Name}/delete" + with client as request: + resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/packages" + + # We should've just sent one closure email since `pkgreq` exists. + assert Email.count() == 1 + + # Make sure it was a closure for the deletion request. + email = Email(1).parse() + expr = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$" + assert re.match(expr, email.headers.get("Subject")) + + +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) + 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."]) + + 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) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + errors = get_errors(resp.text) + expected = "Some error." + assert errors[0].text.strip() == expected + + +def test_packages_post(client: TestClient, user: User, package: Package): + + async def stub_action(request: Request, **kwargs): + 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) + assert resp.status_code == int(HTTPStatus.OK) + + errors = get_successes(resp.text) + expected = "Some success." + assert errors[0].text.strip() == expected + + +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) + 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")} + endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" + with client as request: + resp = request.get(endpoint, cookies=cookies) + 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): + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" + with client as request: + resp = request.post(endpoint, cookies=cookies) + 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")} + endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" + with client as request: + resp = request.post(endpoint, cookies=cookies) + 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.") + 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")} + endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" + with client as request: + resp = request.post(endpoint, data={ + "into": "not_real", + "confirm": True + }, cookies=cookies) + 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")} + endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" + with client as request: + resp = request.post(endpoint, data={ + "into": package.PackageBase.Name, + "confirm": True + }, cookies=cookies) + 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): + pkgname = package.Name + pkgbasename = pkgbase.Name + + # Create a merge request destined for another target. + # This will allow our test code to exercise closing + # such a request after merging the pkgbase in question. + with db.begin(): + pkgreq.ReqTypeID = MERGE_ID + pkgreq.MergeBaseName = target.Name + + # Vote for the package. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + endpoint = f"/pkgbase/{package.PackageBase.Name}/vote" + with client as request: + resp = request.post(endpoint, cookies=cookies) + 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) + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Save these relationships for later comparison. + comments = package.PackageBase.comments.all() + notifs = package.PackageBase.notifications.all() + votes = package.PackageBase.package_votes.all() + + # 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + loc = resp.headers.get("location") + assert loc == f"/pkgbase/{target.Name}" + + # Two emails should've been sent out. + assert Email.count() == 1 + email_body = Email(1).parse().glue() + assert f"Merge Request for {pkgbasename} Accepted" in email_body + + # Assert that the original comments, notifs and votes we setup + # got migrated to target as intended. + assert comments == target.comments.all() + assert notifs == target.notifications.all() + assert votes == target.package_votes.all() + + # ...and that the package got deleted. + package = db.query(Package).filter(Package.Name == pkgname).first() + assert package is None + + # Our previously-made request should have gotten accepted. + assert pkgreq.Status == ACCEPTED_ID + 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() + 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) + 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")} + post_endpoint = f"{endpoint}/keywords" + with client as request: + resp = request.post(post_endpoint, data={ + "keywords": "abc test" + }, cookies=cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + with client as request: + resp = request.get(resp.headers.get("location")) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + keywords = root.xpath('//a[@class="keyword"]') + assert len(keywords) == 2 + expected = ["abc", "test"] + for i, keyword in enumerate(keywords): + assert keyword.text.strip() == expected[i] diff --git a/test/test_pkgmaint.py b/test/test_pkgmaint.py new file mode 100644 index 00000000..5d6a56de --- /dev/null +++ b/test/test_pkgmaint.py @@ -0,0 +1,64 @@ +from typing import List + +import pytest + +from aurweb import db, time +from aurweb.models import Package, PackageBase, User +from aurweb.models.account_type import USER_ID +from aurweb.scripts import pkgmaint + + +@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", + Passwd="testPassword", AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +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") + output.append(pkg) + yield output + + +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]): + 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) + + # Run pkgmaint. + pkgmaint.main() + + # 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 diff --git a/test/test_popupdate.py b/test/test_popupdate.py new file mode 100644 index 00000000..ce3f9f11 --- /dev/null +++ b/test/test_popupdate.py @@ -0,0 +1,12 @@ +import pytest + +from aurweb.scripts import popupdate + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_popupdate(): + popupdate.main() diff --git a/test/test_ratelimit.py b/test/test_ratelimit.py new file mode 100644 index 00000000..859adea9 --- /dev/null +++ b/test/test_ratelimit.py @@ -0,0 +1,117 @@ +from unittest import mock + +import pytest + +from redis.client import Pipeline + +from aurweb import config, db, logging +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__) + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def pipeline(): + redis = redis_connection() + pipeline = redis.pipeline() + + pipeline.delete("ratelimit-ws:127.0.0.1") + pipeline.delete("ratelimit:127.0.0.1") + pipeline.execute() + + yield pipeline + + +config_getint = config.getint + + +def mock_config_getint(section: str, key: str): + if key == "request_limit": + return 4 + elif key == "window_length": + return 100 + return config_getint(section, key) + + +config_getboolean = config.getboolean + + +def mock_config_getboolean(return_value: int = 0): + def fn(section: str, key: str): + if section == "ratelimit" and key == "cache": + return return_value + return config_getboolean(section, key) + return fn + + +config_get = config.get + + +def mock_config_get(return_value: str = "none"): + def fn(section: str, key: str): + 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 + path if a real Redis server is configured. Otherwise, + it'll use the database. """ + + # We'll need a Request for everything here. + request = Request() + + # Run check_ratelimit for our request_limit. These should succeed. + for i in range(4): + assert not check_ratelimit(request) + + # This check_ratelimit should fail, being the 4001th request. + assert check_ratelimit(request) + + # Delete the Redis keys. + host = request.client.host + pipeline.delete(f"ratelimit-ws:{host}") + pipeline.delete(f"ratelimit:{host}") + one, two = pipeline.execute() + assert one and two + + # Should be good to go again! + assert not check_ratelimit(request) + + +@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): + + # We'll need a Request for everything here. + request = Request() + + # Run check_ratelimit for our request_limit. These should succeed. + for i in range(4): + assert not check_ratelimit(request) + + # This check_ratelimit should fail, being the 4001th request. + assert check_ratelimit(request) + + # Delete the ApiRateLimit record. + with db.begin(): + db.delete(db.query(ApiRateLimit).first()) + + # Should be good to go again! + assert not check_ratelimit(request) diff --git a/test/test_redis.py b/test/test_redis.py new file mode 100644 index 00000000..82aebb57 --- /dev/null +++ b/test/test_redis.py @@ -0,0 +1,40 @@ +from unittest import mock + +import pytest + +import aurweb.config + +from aurweb.redis import redis_connection + + +@pytest.fixture +def rediss(): + """ Create a RedisStub. """ + def mock_get(section, key): + return "none" + + with mock.patch("aurweb.config.get", side_effect=mock_get): + aurweb.config.rehash() + redis = redis_connection() + aurweb.config.rehash() + + yield redis + + +def test_redis_stub(rediss): + # We don't yet have a test key set. + assert rediss.get("test") is None + + # Set the test key to abc. + rediss.set("test", "abc") + assert rediss.get("test").decode() == "abc" + + # Test expire. + rediss.expire("test", 0) + assert rediss.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 diff --git a/test/test_relation_type.py b/test/test_relation_type.py new file mode 100644 index 00000000..263ae1ec --- /dev/null +++ b/test/test_relation_type.py @@ -0,0 +1,34 @@ +import pytest + +from aurweb import db +from aurweb.models.relation_type import RelationType + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_relation_type_creation(): + with db.begin(): + relation_type = db.create(RelationType, Name="test-relation") + + assert bool(relation_type.ID) + assert relation_type.Name == "test-relation" + + with db.begin(): + db.delete(relation_type) + + +def test_relation_types(): + conflicts = db.query(RelationType, RelationType.Name == "conflicts").first() + assert conflicts is not None + assert conflicts.Name == "conflicts" + + provides = db.query(RelationType, RelationType.Name == "provides").first() + assert provides is not None + assert provides.Name == "provides" + + replaces = db.query(RelationType, RelationType.Name == "replaces").first() + assert replaces is not None + assert replaces.Name == "replaces" diff --git a/test/test_rendercomment.py b/test/test_rendercomment.py new file mode 100644 index 00000000..bf4009fd --- /dev/null +++ b/test/test_rendercomment.py @@ -0,0 +1,201 @@ +from unittest import mock + +import pytest + +from aurweb import config, db, logging, 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__) +aur_location = config.get("options", "aur_location") + + +@pytest.fixture(autouse=True) +def setup(db_test, git: GitRepository): + config_get = config.get + + def mock_config_get(section: str, key: str) -> str: + if section == "serve" and key == "repo-path": + return git.file_lock.path + elif section == "options" and key == "commit_uri": + return "/cgit/aur.git/log/?h=%s&id=%s" + return config_get(section, key) + + with mock.patch("aurweb.config.get", side_effect=mock_config_get): + yield + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create(User, Username="test", Email="test@example.org", + Passwd=str(), AccountTypeID=USER_ID) + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + now = time.utcnow() + with db.begin(): + 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") + yield package + + +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) + if render: + update_comment_render(comment) + return comment + + +def test_comment_rendering(user: User, pkgbase: PackageBase): + text = "Hello world! This is a comment." + comment = create_comment(user, pkgbase, text) + expected = f"

{text}

" + assert comment.RenderedComment == expected + + +def test_rendercomment_main(user: User, pkgbase: PackageBase): + text = "Hello world! This is a comment." + comment = create_comment(user, pkgbase, text, False) + + args = ["aurweb-rendercomment", str(comment.ID)] + with mock.patch("sys.argv", args): + rendercomment.main() + db.refresh(comment) + + expected = f"

{text}

" + assert comment.RenderedComment == expected + + +def test_markdown_conversion(user: User, pkgbase: PackageBase): + text = "*Hello* [world](https://aur.archlinux.org)!" + comment = create_comment(user, pkgbase, text) + expected = ('

Hello ' + 'world!

') + assert comment.RenderedComment == expected + + +def test_html_sanitization(user: User, pkgbase: PackageBase): + text = '' + comment = create_comment(user, pkgbase, text) + expected = '<script>alert("XSS!")</script>' + assert comment.RenderedComment == expected + + +def test_link_conversion(user: User, pkgbase: PackageBase): + text = """\ +Visit https://www.archlinux.org/#_test_. +Visit *https://www.archlinux.org/*. +Visit . +Visit `https://www.archlinux.org/`. +Visit [Arch Linux](https://www.archlinux.org/). +Visit [Arch Linux][arch]. +[arch]: https://www.archlinux.org/\ +""" + comment = create_comment(user, pkgbase, text) + expected = '''\ +

Visit \ +https://www.archlinux.org/#_test_. +Visit https://www.archlinux.org/. +Visit https://www.archlinux.org/. +Visit https://www.archlinux.org/. +Visit Arch Linux. +Visit Arch Linux.

\ +''' + assert comment.RenderedComment == expected + + +def test_git_commit_link(git: GitRepository, user: User, package: Package): + commit_hash = git.commit(package, "Initial commit.") + logger.info(f"Created commit: {commit_hash}") + logger.info(f"Short hash: {commit_hash[:7]}") + + text = f"""\ +{commit_hash} +{commit_hash[:7]} +x.{commit_hash}.x +{commit_hash}x +0123456789abcdef +`{commit_hash}` +http://example.com/{commit_hash}\ +""" + comment = create_comment(user, package.PackageBase, text) + + pkgname = package.PackageBase.Name + cgit_path = f"/cgit/aur.git/log/?h={pkgname}&" + expected = f"""\ +

{commit_hash[:12]} +{commit_hash[:7]} +x.{commit_hash[:12]}.x +{commit_hash}x +0123456789abcdef +{commit_hash} +\ +http://example.com/{commit_hash}\ +\ +

\ +""" + assert comment.RenderedComment == expected + + +def test_flyspray_issue_link(user: User, pkgbase: PackageBase): + text = """\ +FS#1234567. +*FS#1234* +FS# +XFS#1 +`FS#1234` +https://archlinux.org/?test=FS#1234\ +""" + comment = create_comment(user, pkgbase, text) + + expected = """\ +

FS#1234567. +FS#1234 +FS# +XFS#1 +FS#1234 +\ +https://archlinux.org/?test=FS#1234\ +\ +

\ +""" + assert comment.RenderedComment == expected + + +def test_lower_headings(user: User, pkgbase: PackageBase): + text = """\ +# One +## Two +### Three +#### Four +##### Five +###### Six\ +""" + comment = create_comment(user, pkgbase, text) + + expected = """\ +
One
+
Two
+
Three
+
Four
+
Five
+
Six
\ +""" + assert comment.RenderedComment == expected diff --git a/test/test_request_type.py b/test/test_request_type.py new file mode 100644 index 00000000..0bc86319 --- /dev/null +++ b/test/test_request_type.py @@ -0,0 +1,42 @@ +import pytest + +from aurweb import db +from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID, RequestType + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_request_type_creation(): + with db.begin(): + request_type = db.create(RequestType, Name="Test Request") + + assert bool(request_type.ID) + assert request_type.Name == "Test Request" + + with db.begin(): + db.delete(request_type) + + +def test_request_type_null_name_returns_empty_string(): + with db.begin(): + request_type = db.create(RequestType) + + assert bool(request_type.ID) + assert request_type.Name == str() + + with db.begin(): + db.delete(request_type) + + +def test_request_type_name_display(): + deletion = db.query(RequestType, RequestType.ID == DELETION_ID).first() + assert deletion.name_display() == "Deletion" + + orphan = db.query(RequestType, RequestType.ID == ORPHAN_ID).first() + assert orphan.name_display() == "Orphan" + + merge = db.query(RequestType, RequestType.ID == MERGE_ID).first() + assert merge.name_display() == "Merge" diff --git a/test/test_requests.py b/test/test_requests.py new file mode 100644 index 00000000..113368f0 --- /dev/null +++ b/test/test_requests.py @@ -0,0 +1,759 @@ +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.package_notification import PackageNotification +from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID +from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID +from aurweb.packages.requests import ClosureFactory +from aurweb.requests.util import get_pkgreq_by_id +from aurweb.testing.email import Email +from aurweb.testing.html import get_errors, parse_root +from aurweb.testing.requests import Request + + +@pytest.fixture(autouse=True) +def setup(db_test) -> None: + """ Setup the database. """ + return + + +@pytest.fixture +def client() -> TestClient: + """ Yield a TestClient. """ + yield TestClient(app=asgi.app) + + +def create_user(username: str, email: str) -> User: + """ + Create a user based on `username` and `email`. + + :param username: User.Username + :param email: User.Email + :return: User instance + """ + with db.begin(): + user = db.create(User, Username=username, Email=email, + Passwd="testPassword", AccountTypeID=USER_ID) + return user + + +@pytest.fixture +def user() -> User: + """ 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. """ + cookies = {"AURSID": user.login(Request(), "testPassword")} + user.cookies = cookies + yield user + + +@pytest.fixture +def user2() -> User: + """ 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. """ + cookies = {"AURSID": user2.login(Request(), "testPassword")} + user2.cookies = cookies + yield user2 + + +@pytest.fixture +def maintainer() -> User: + """ 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) + yield maintainer + + +@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}") + packages_.append(package) + + yield packages_ + + +@pytest.fixture +def requests(user: 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()) + 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") + with db.begin(): + user.AccountTypeID = TRUSTED_USER_ID + cookies = {"AURSID": user.login(Request(), "testPassword")} + user.cookies = cookies + yield user + + +def create_pkgbase(user: User, name: str) -> PackageBase: + """ + Create a package base based on `user` and `name`. + + This function also creates a matching Package record. + + :param user: User instance + :param name: PackageBase.Name + :return: PackageBase instance + """ + now = time.utcnow() + with db.begin(): + 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. """ + pkgbase = create_pkgbase(user, "test-package") + yield pkgbase + + +@pytest.fixture +def target(user: User) -> PackageBase: + """ Yield a merge target (package base). """ + with db.begin(): + 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: + """ + Create a package request based on `reqtype_id`, `user`, + `pkgbase` and `comments`. + + :param reqtype_id: RequestType.ID + :param user: User instance + :param pkgbase: PackageBase instance + :param comments: PackageRequest.Comments + :return: PackageRequest instance + """ + 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()) + return pkgreq + + +@pytest.fixture +def pkgreq(user: User, pkgbase: PackageBase): + """ 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`. """ + 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. """ + endpoint = f"/pkgbase/{pkgbase.Name}/request" + with client as request: + resp = request.get(endpoint, cookies=auser.cookies) + 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. """ + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = pkgbase.requests.first() + assert pkgreq is not None + assert pkgreq.ReqTypeID == DELETION_ID + assert pkgreq.Status == PENDING_ID + + # A RequestOpenNotification should've been sent out. + assert Email.count() == 1 + email = Email(1) + expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+$" % pkgreq.ID + 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. """ + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Check the pkgreq record got created and accepted. + pkgreq = db.query(PackageRequest).first() + assert pkgreq is not None + assert pkgreq.ReqTypeID == DELETION_ID + assert pkgreq.Status == ACCEPTED_ID + + # Should've gotten two emails. + assert Email.count() == 2 + + # A RequestOpenNotification should've been sent out. + email = Email(1) + expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+$" % pkgreq.ID + assert re.match(expr, email.headers.get("Subject")) + + # Check the content of the close notification. + email = Email(2) + expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+ Accepted$" % pkgreq.ID + 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. """ + caplog.set_level(DEBUG) + + now = time.utcnow() + auto_delete_age = config.getint("options", "auto_delete_age") + with db.begin(): + pkgbase.ModifiedTS = now - auto_delete_age + 100 + + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + 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 + + # A RequestOpenNotification should've been sent out. + assert Email.count() == 2 + Email.dump() + + # Check the content of the open notification. + email = Email(1) + expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+$" % pkgreq.ID + assert re.match(expr, email.headers.get("Subject")) + + # Check the content of the close notification. + email = Email(2) + expr = r"^\[PRQ#%d\] Deletion Request for [^ ]+ Accepted$" % pkgreq.ID + assert re.match(expr, email.headers.get("Subject")) + + # Check logs. + expr = r"New request #\d+ is marked for auto-deletion." + 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. """ + endpoint = f"/pkgbase/{pkgbase.Name}/request" + data = { + "type": "merge", + "merge_into": target.Name, + "comments": "Test request.", + } + with client as request: + resp = request.post(endpoint, data=data, cookies=auser.cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = pkgbase.requests.first() + assert pkgreq is not None + assert pkgreq.ReqTypeID == MERGE_ID + assert pkgreq.Status == PENDING_ID + assert pkgreq.MergeBaseName == target.Name + + # A RequestOpenNotification should've been sent out. + assert Email.count() == 1 + email = Email(1) + expr = r"^\[PRQ#%d\] Merge Request for [^ ]+$" % pkgreq.ID + 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. """ + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = pkgbase.requests.first() + assert pkgreq is not None + assert pkgreq.ReqTypeID == ORPHAN_ID + assert pkgreq.Status == PENDING_ID + + # A RequestOpenNotification should've been sent out. + assert Email.count() == 1 + + email = Email(1) + expr = r"^\[PRQ#%d\] Orphan Request for [^ ]+$" % pkgreq.ID + 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. """ + # `pkgreq`.ReqTypeID is already DELETION_ID. + create_request(DELETION_ID, user, pkgbase, "Other request.") + + # Create a notification record for another user. They should then + # also receive a DeleteNotification. + user2 = create_user("test2", "test2@example.org") + create_notification(user2, pkgbase) + + endpoint = f"/pkgbase/{pkgbase.Name}/delete" + comments = "Test closure." + data = {"comments": comments, "confirm": True} + with client as request: + resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/packages" + + # Ensure that `pkgreq`.ClosureComment was left alone when specified. + assert pkgreq.ClosureComment == comments + + # We should've gotten three emails. Two accepted requests and + # a DeleteNotification. + assert Email.count() == 3 + + # Both requests should have gotten accepted and had a notification + # sent out for them. + for i in range(Email.count() - 1): + email = Email(i + 1).parse() + expr = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$" + assert re.match(expr, email.headers.get("Subject")) + + # We should've also had a DeleteNotification sent out. + 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) + assert body in email.body + + +def test_deletion_autorequest(client: TestClient, tu_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) + 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 "[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. """ + with db.begin(): + pkgreq.ReqTypeID = MERGE_ID + pkgreq.MergeBaseName = target.Name + + other_target = create_pkgbase(user, "other-target") + other_request = create_request(MERGE_ID, user, pkgbase, "Other request.") + other_target2 = create_pkgbase(user, "other-target2") + other_request2 = create_request(MERGE_ID, user, pkgbase, "Other request2.") + with db.begin(): + other_request.MergeBaseName = other_target.Name + other_request2.MergeBaseName = other_target2.Name + + # `pkgreq`.ReqTypeID is already DELETION_ID. + endpoint = f"/pkgbase/{pkgbase.Name}/merge" + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{target.Name}" + + # Ensure that `pkgreq`.ClosureComment was left alone when specified. + assert pkgreq.ClosureComment == comments + + # We should've gotten 3 emails: an accepting and two rejections. + assert Email.count() == 3 + + # Assert specific IDs match up in the subjects. + accepted = Email(1).parse() + subj = r"^\[PRQ#%d\] Merge Request for [^ ]+ Accepted$" % pkgreq.ID + assert re.match(subj, accepted.headers.get("Subject")) + + # In the accepted case, we already supplied a closure comment, + # which stops one from being autogenerated by the algorithm. + assert "[Autogenerated]" not in accepted.body + + # Test rejection emails, which do have autogenerated closures. + rejected = Email(2).parse() + subj = r"^\[PRQ#%d\] Merge Request for [^ ]+ Rejected$" % other_request.ID + assert re.match(subj, rejected.headers.get("Subject")) + assert "[Autogenerated]" in rejected.body + + rejected = Email(3).parse() + subj = r"^\[PRQ#%d\] Merge Request for [^ ]+ Rejected$" % other_request2.ID + assert re.match(subj, rejected.headers.get("Subject")) + 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. """ + 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} + with client as request: + resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{target.Name}" + + # Should've gotten one email; an [Autogenerated] one. + 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 "[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. """ + idle_time = config.getint("options", "request_idle_time") + now = time.utcnow() + with db.begin(): + pkgreq.ReqTypeID = ORPHAN_ID + # Set the request time so it's seen as due (idle_time has passed). + pkgreq.RequestTS = now - idle_time - 10 + + endpoint = f"/pkgbase/{pkgbase.Name}/disown" + comments = "Test orphan closure." + data = {"comments": comments, "confirm": True} + with client as request: + resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + # Ensure that `pkgreq`.ClosureComment was left alone when specified. + assert pkgreq.ClosureComment == comments + + # Check the email we expect. + assert Email.count() == 1 + email = Email(1).parse() + subj = r"^\[PRQ#%d\] Orphan Request for [^ ]+ Accepted$" % pkgreq.ID + assert re.match(subj, email.headers.get("Subject")) + + +def test_request_post_orphan_autogenerated_closure(client: TestClient, + tu_user: User, + pkgbase: PackageBase, + pkgreq: PackageRequest): + idle_time = config.getint("options", "request_idle_time") + now = time.utcnow() + with db.begin(): + pkgreq.ReqTypeID = ORPHAN_ID + # Set the request time so it's seen as due (idle_time has passed). + pkgreq.RequestTS = now - idle_time - 10 + + endpoint = f"/pkgbase/{pkgbase.Name}/disown" + data = {"confirm": True} + with client as request: + resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + assert Email.count() == 1 + email = Email(1) + expr = r"^\[PRQ#\d+\] Orphan Request for .+ Accepted$" + assert re.match(expr, email.headers.get("Subject")) + + expr = r"\[Autogenerated\] Accepted orphan for .+\." + 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. """ + caplog.set_level(DEBUG) + now = time.utcnow() + auto_orphan_age = config.getint("options", "auto_orphan_age") + with db.begin(): + pkgbase.OutOfDateTS = now - auto_orphan_age - 100 + + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + pkgreq = pkgbase.requests.first() + assert pkgreq is not None + assert pkgreq.ReqTypeID == ORPHAN_ID + + # A Request(Open|Close)Notification should've been sent out. + assert Email.count() == 2 + + # Check the first email; should be our open request. + email = Email(1) + expr = r"^\[PRQ#%d\] Orphan Request for [^ ]+$" % pkgreq.ID + assert re.match(expr, email.headers.get("Subject")) + + # And the second should be the automated closure. + email = Email(2) + expr = r"^\[PRQ#%d\] Orphan Request for [^ ]+ Accepted$" % pkgreq.ID + assert re.match(expr, email.headers.get("Subject")) + + # Check logs. + expr = r"New request #\d+ is marked for auto-orphan." + assert re.search(expr, caplog.text) + + +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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" + + # As the pkgbase maintainer, disowning the package just ends up + # either promoting the lowest priority comaintainer or removing + # the associated maintainer relationship altogether. + 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. """ + endpoint = f"/pkgbase/{pkgbase.Name}/disown" + data = {"confirm": True} + with client as request: + resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + + errors = get_errors(resp.text) + expected = r"^No due existing orphan requests to accept for .+\.$" + assert re.match(expected, errors[0].text.strip()) + + assert Email.count() == 0 + + +def test_closure_factory_invalid_reqtype_id(): + """ Test providing an invalid reqtype_id raises NotImplementedError. """ + automated = ClosureFactory() + match = r"^Unsupported '.+' value\.$" + with pytest.raises(NotImplementedError, match=match): + automated.get_closure(666, None, None, None, ACCEPTED_ID) + with pytest.raises(NotImplementedError, match=match): + automated.get_closure(666, None, None, None, REJECTED_ID) + + +def test_pkgreq_by_id_not_found(): + with pytest.raises(HTTPException): + get_pkgreq_by_id(0) + + +def test_requests_unauthorized(client: TestClient): + with client as request: + resp = request.get("/requests", allow_redirects=False) + 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")} + 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) + 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: + resp = request.get("/requests", params={ + "O": 50 # Page 2 + }, cookies=cookies) + 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. + + +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) + assert resp.status_code == int(HTTPStatus.OK) + + # As the user who creates all of the requests, we should see all of them. + # However, we are not allowed to accept any of them ourselves. + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == defaults.PP + + # 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] + assert last_row.text.strip() == "Close" + + +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) + assert resp.status_code == int(HTTPStatus.OK) + + +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) + 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): + 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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/" + + +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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + assert pkgreq.Status == REJECTED_ID + assert pkgreq.Closer == user + assert pkgreq.ClosureComment == str() + + +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) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + assert pkgreq.Status == REJECTED_ID + assert pkgreq.Closer == user + assert pkgreq.ClosureComment == str() diff --git a/test/test_routes.py b/test/test_routes.py new file mode 100644 index 00000000..85d30c02 --- /dev/null +++ b/test/test_routes.py @@ -0,0 +1,162 @@ +import re +import urllib.parse + +from http import HTTPStatus + +import lxml.etree +import pytest + +from fastapi.testclient import TestClient + +from aurweb import 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 + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=app) + + +@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_index(client: TestClient): + """ 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. + + CSP: Content-Security-Policy + XCTO: X-Content-Type-Options + RP: Referrer-Policy + XFO: X-Frame-Options + """ + # Use `with` to trigger FastAPI app events. + with client as req: + response = req.get("/") + assert response.status_code == int(HTTPStatus.OK) + assert response.headers.get("Content-Security-Policy") is not None + assert response.headers.get("X-Content-Type-Options") == "nosniff" + assert response.headers.get("Referrer-Policy") == "same-origin" + assert response.headers.get("X-Frame-Options") == "SAMEORIGIN" + + +def test_favicon(client: TestClient): + """ Test the favicon route at '/favicon.ico'. """ + with client as request: + response1 = request.get("/static/images/favicon.ico") + response2 = request.get("/favicon.ico") + 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": "/" + } + 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" + } + 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": "/" + } + + sid = user.login(Request(), "testPassword") + assert sid is not None + + with client as req: + response = req.post("/language", data=post_data, + cookies={"AURSID": sid}) + 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. """ + next = urllib.parse.quote_plus("/") + 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) + assert response.headers.get("location") == f"/?{q}" + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + +def test_error_messages(client: TestClient): + with client as request: + response1 = request.get("/thisroutedoesnotexist") + response2 = request.get("/raisefivethree") + assert response1.status_code == int(HTTPStatus.NOT_FOUND) + assert response2.status_code == int(HTTPStatus.SERVICE_UNAVAILABLE) + + +def test_nonce_csp(client: TestClient): + with client as request: + response = request.get("/") + data = response.headers.get("Content-Security-Policy") + nonce = next(field for field in data.split("; ") if "nonce" in field) + match = re.match(r"^script-src .*'nonce-([a-fA-F0-9]{8})' .*$", nonce) + nonce = match.group(1) + assert nonce is not None and len(nonce) == 8 + + parser = lxml.etree.HTMLParser(recover=True) + root = lxml.etree.fromstring(response.text, parser=parser) + + nonce_verified = False + scripts = root.xpath("//script") + for script in scripts: + if script.text is not None: + assert "nonce" in script.keys() + if not (nonce_verified := (script.get("nonce") == nonce)): + break + assert nonce_verified is True + + +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) + assert response.headers.get("location") == "/test?key=value&key2=value2" diff --git a/test/test_rpc.py b/test/test_rpc.py new file mode 100644 index 00000000..6063a26f --- /dev/null +++ b/test/test_rpc.py @@ -0,0 +1,784 @@ +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.models.account_type import USER_ID +from aurweb.models.license import License +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_dependency import PackageDependency +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.user import User +from aurweb.redis import redis_connection + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) + + +@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) + 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) + 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) + yield user + + +@pytest.fixture +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/") + 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/") + 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/") + output.append(pkg) + + 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") + 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/") + output.append(pkg) + + # Setup a few more related records on the first package: + # a license, some keywords and some votes. + with db.begin(): + lic = db.create(License, Name="GPL") + db.create(PackageLicense, Package=output[0], License=lic) + + for keyword in ["big-chungus", "smol-chungus", "sizeable-chungus"]: + 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) + scripts.popupdate.run_single(output[0].PackageBase) + + yield output + + +@pytest.fixture +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") + output.append(dep) + + 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") + output.append(dep) + + 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") + output.append(dep) + + yield output + + +@pytest.fixture +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") + output.append(rel) + + 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") + output.append(rel) + + 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(autouse=True) +def setup(db_test): + # Create some extra package relationships. + pass + + +@pytest.fixture +def pipeline(): + redis = redis_connection() + pipeline = redis.pipeline() + + # The 'testclient' host is used when requesting the app + # via fastapi.testclient.TestClient. + pipeline.delete("ratelimit-ws:testclient") + pipeline.delete("ratelimit:testclient") + pipeline.execute() + + yield pipeline + + +def test_rpc_documentation(client: TestClient): + with client as request: + resp = request.get("/rpc") + assert resp.status_code == int(HTTPStatus.OK) + assert "aurweb RPC Interface" in resp.text + + +def test_rpc_documentation_missing(): + config_get = config.get + + def mock_get(section: str, key: str) -> str: + if section == "options" and key == "aurwebdir": + return "/missing" + return config_get(section, key) + + with mock.patch("aurweb.config.get", side_effect=mock_get): + config.rehash() + expr = r"^doc/rpc\.html could not be read$" + with pytest.raises(OSError, match=expr): + rpc.documentation() + config.rehash() + + +def test_rpc_singular_info(client: TestClient, + user: User, + packages: List[Package], + depends: List[PackageDependency], + relations: List[PackageRelation]): + # 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" + ] + }], + "resultcount": 1, + "type": "multiinfo" + } + + # Make dummy request. + with client as request: + 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) + + # Remove the FirstSubmitted LastModified, ID and PackageBaseID keys from + # reponse, as the key's values aren't guaranteed to match between the two + # (the keys are already removed from 'expected_data'). + for i in ["FirstSubmitted", "LastModified", "ID", "PackageBaseID"]: + response_data["results"][0].pop(i) + + # Validate that the new dictionaries are the same. + assert response_data == expected_data + + +def test_rpc_nonexistent_package(client: TestClient): + # Make dummy request. + with client as request: + response = request.get("/rpc/?v=5&type=info&arg=nonexistent-package") + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + assert response_data["resultcount"] == 0 + + +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 + }) + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + for i in response_data["results"]: + request_packages.remove(i["Name"]) + + assert request_packages == [] + + +def test_rpc_mixedargs(client: TestClient, packages: List[Package]): + # Make dummy request. + response1_packages = ["gluggly-chungus"] + response2_packages = ["gluggly-chungus", "chungy-chungus"] + + 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") + 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") + assert response1.status_code == int(HTTPStatus.OK) + + # Load request response into Python dictionary. + response1_data = orjson.loads(response1.content.decode()) + response2_data = orjson.loads(response2.content.decode()) + + # Validate data. + for i in response1_data["results"]: + response1_packages.remove(i["Name"]) + + for i in response2_data["results"]: + response2_packages.remove(i["Name"]) + + for i in [response1_packages, response2_packages]: + assert i == [] + + +def test_rpc_no_dependencies_omits_key(client: TestClient, user: 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' + } + + # Make dummy request. + with client as request: + response = request.get("/rpc", params={ + "v": 5, "type": "info", "arg": "chungy-chungus" + }) + response_data = orjson.loads(response.content.decode()) + + # Remove inconsistent keys. + for i in ["ID", "PackageBaseID", "FirstSubmitted", "LastModified"]: + response_data["results"][0].pop(i) + + assert response_data == expected_response + + +def test_rpc_bad_type(client: TestClient): + # Define expected response. + expected_data = { + '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" + }) + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + assert expected_data == response_data + + +def test_rpc_bad_version(client: TestClient): + # Define expected response. + expected_data = { + '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" + }) + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + assert expected_data == response_data + + +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.' + } + + # Make dummy request. + with client as request: + response = request.get("/rpc", params={ + "type": "info", + "arg": "big-chungus" + }) + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + assert expected_data == response_data + + +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.' + } + + # Make dummy request. + with client as request: + response = request.get("/rpc", params={"v": 5, "arg": "big-chungus"}) + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + assert expected_data == response_data + + +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.' + } + + # Make dummy request. + with client as request: + response = request.get("/rpc", params={"v": 5, "type": "info"}) + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + assert expected_data == response_data + + +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" + }) + + # Load request response into Python dictionary. + response_data = orjson.loads(response.content.decode()) + + # Validate data. + assert response_data["results"][0]["Maintainer"] is None + + +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) + data = response.json() + assert data == ["big-chungus"] + + params["arg"] = "chungy" + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data == ["chungy-chungus"] + + # Test no arg supplied. + del params["arg"] + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data == [] + + +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) + data = response.json() + assert data == ["other-pkg"] + + # Test non-existent Package. + params["arg"] = "nonexistent" + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data == [] + + # Test no arg supplied. + del params["arg"] + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data == [] + + +def mock_config_getint(section: str, key: str): + if key == "request_limit": + return 4 + elif key == "window_length": + return 100 + return config.getint(section, key) + + +@mock.patch("aurweb.config.getint", side_effect=mock_config_getint) +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): + # The first 4 requests should be good. + with client as request: + response = request.get("/rpc", params=params) + assert response.status_code == int(HTTPStatus.OK) + + # The fifth request should be banned. + with client as request: + response = request.get("/rpc", params=params) + assert response.status_code == int(HTTPStatus.TOO_MANY_REQUESTS) + + # Delete the cached records. + pipeline.delete("ratelimit-ws:testclient") + pipeline.delete("ratelimit:testclient") + one, two = pipeline.execute() + assert one and two + + # The new first request should be good. + with client as request: + response = request.get("/rpc", params=params) + assert response.status_code == int(HTTPStatus.OK) + + +def test_rpc_etag(client: TestClient, packages: List[Package]): + params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} + + with client as request: + response1 = request.get("/rpc", params=params) + with client as request: + response2 = request.get("/rpc", params=params) + + assert response1.headers.get("ETag") is not None + assert response1.headers.get("ETag") != str() + assert response1.headers.get("ETag") == response2.headers.get("ETag") + + +def test_rpc_search_arg_too_small(client: TestClient): + params = {"v": 5, "type": "search", "arg": "b"} + with client as request: + response = request.get("/rpc", params=params) + assert response.status_code == int(HTTPStatus.OK) + assert response.json().get("error") == "Query arg too small." + + +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) + assert response.status_code == int(HTTPStatus.OK) + + data = response.json() + assert data.get("resultcount") == 1 + + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + # 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'' + + # No args on non-m by types return an error. + del params["arg"] + with client as request: + response = request.get("/rpc", params=params) + assert response.json().get("error") == "No request type/data specified." + + +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) + data = response.json() + + # 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" + ] + assert names == expected_results + + # Search for a non-existent maintainer, giving us zero packages. + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 0 + + with db.begin(): + packages[0].PackageBase.Maintainer = None + + # A missing arg still succeeds, but it returns all orphans. + # Just verify that we receive no error and the orphaned result. + params.pop("arg") + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + 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" + } + 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_makedepends(client: TestClient, packages: List[Package], + depends: List[PackageDependency]): + params = { + "v": 5, + "type": "search", + "by": "makedepends", + "arg": "chungus-makedepends" + } + 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_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() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_checkdepends(client: TestClient, packages: List[Package], + depends: List[PackageDependency]): + params = { + "v": 5, + "type": "search", + "by": "checkdepends", + "arg": "chungus-checkdepends" + } + 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_incorrect_by(client: TestClient): + params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} + with client as request: + response = request.get("/rpc", params=params) + assert response.json().get("error") == "Incorrect by field specified." + + +def test_rpc_jsonp_callback(client: TestClient): + """ 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" + } + 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 + + # Test an invalid callback name; we get an application/json error. + params["callback"] = "jsonCallback!" + with client as request: + response = request.get("/rpc", params=params) + assert response.headers.get("content-type") == "application/json" + assert response.json().get("error") == "Invalid callback name." diff --git a/test/test_rss.py b/test/test_rss.py new file mode 100644 index 00000000..cef6a46f --- /dev/null +++ b/test/test_rss.py @@ -0,0 +1,102 @@ +from http import HTTPStatus + +import lxml.etree +import pytest + +from fastapi.testclient import TestClient + +from aurweb import db, logging, 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__) + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def client(): + yield TestClient(app=app) + + +@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) + + +@pytest.fixture +def packages(user): + pkgs = [] + now = time.utcnow() + + # Create 101 packages; we limit 100 on RSS feeds. + 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)) + pkg = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) + pkgs.append(pkg) + yield pkgs + + +def parse_root(xml): + return lxml.etree.fromstring(xml) + + +def test_rss(client, user, packages): + with client as request: + response = request.get("/rss/") + assert response.status_code == int(HTTPStatus.OK) + + # 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. + packages = packages[:100] + + root = parse_root(response.content) + items = root.xpath("//channel/item") + assert len(items) == 100 + + for i, item in enumerate(items): + title = next(iter(item.xpath('./title'))) + logger.debug(f"title: '{title.text}' vs name: '{packages[i].Name}'") + assert title.text == packages[i].Name + + +def test_rss_modified(client, user, packages): + with client as request: + response = request.get("/rss/modified") + assert response.status_code == int(HTTPStatus.OK) + + # 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. + packages = packages[:100] + + root = parse_root(response.content) + items = root.xpath("//channel/item") + assert len(items) == 100 + + for i, item in enumerate(items): + 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 new file mode 100644 index 00000000..edae57f9 --- /dev/null +++ b/test/test_session.py @@ -0,0 +1,80 @@ +""" Test our Session model. """ +from unittest import mock + +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db, time +from aurweb.models.account_type import USER_ID +from aurweb.models.session import Session, generate_unique_sid +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", + 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()) + yield session + + +def test_session(user: User, session: Session): + assert session.SessionID == "testSession" + assert session.UsersID == user.ID + + +def test_session_cs(): + """ 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) + + with db.begin(): + session_cs = db.create(Session, User=user2, SessionID="TESTSESSION", + LastUpdateTS=time.utcnow()) + + assert session_cs.SessionID == "TESTSESSION" + assert session_cs.SessionID != "testSession" + + +def test_session_user_association(user: User, session: Session): + # Make sure that the Session user attribute is correct. + assert session.User == user + + +def test_session_null_user_raises(): + with pytest.raises(IntegrityError): + Session() + + +def test_generate_unique_sid(session: Session): + # Mock up aurweb.models.session.generate_sid by returning + # sids[i % 2] from 0 .. n. This will swap between each sid + # between each call. + sids = ["testSession", "realSession"] + i = 0 + + def mock_generate_sid(length): + nonlocal i + sid = sids[i % 2] + i += 1 + return sid + + with mock.patch("aurweb.util.make_random_string", mock_generate_sid): + assert generate_unique_sid() == "realSession" diff --git a/test/test_spawn.py b/test/test_spawn.py new file mode 100644 index 00000000..195eb897 --- /dev/null +++ b/test/test_spawn.py @@ -0,0 +1,149 @@ +import os +import tempfile + +from typing import Tuple +from unittest import mock + +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" +} + + +class FakeProcess: + """ Fake a subprocess.Popen return object. """ + + returncode = 0 + stdout = b'' + stderr = b'' + + def __init__(self, *args, **kwargs): + """ We need this constructor to remain compatible with Popen. """ + pass + + def communicate(self) -> Tuple[bytes, bytes]: + return (self.stdout, self.stderr) + + def terminate(self) -> None: + raise Exception("Fake termination.") + + def wait(self) -> int: + return self.returncode + + +class MockFakeProcess: + """ FakeProcess construction helper to be used in mocks. """ + + def __init__(self, return_code: int = 0, stdout: bytes = b'', + stderr: bytes = b''): + self.returncode = return_code + self.stdout = stdout + self.stderr = stderr + + def process(self, *args, **kwargs) -> FakeProcess: + proc = FakeProcess() + proc.returncode = self.returncode + proc.stdout = self.stdout + proc.stderr = self.stderr + 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() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + aurweb.spawn.generate_nginx_config() + nginx_config_path = os.path.join(ctx.name, "nginx.conf") + 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}" + ] + for expected in expected_content: + assert expected in nginx_config + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +def test_spawn_start_stop(): + ctx = tempfile.TemporaryDirectory() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + aurweb.spawn.start() + aurweb.spawn.stop() + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +@mock.patch("aurweb.spawn.children", [MockFakeProcess().process()]) +def test_spawn_start_noop_with_children(): + aurweb.spawn.start() + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +@mock.patch("aurweb.spawn.children", [MockFakeProcess().process()]) +def test_spawn_stop_terminate_failure(): + ctx = tempfile.TemporaryDirectory() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + match = r"^Errors terminating the child processes" + with pytest.raises(aurweb.spawn.ProcessExceptions, match=match): + aurweb.spawn.stop() + + +@mock.patch("aurweb.spawn.asgi_backend", "uvicorn") +@mock.patch("aurweb.spawn.verbosity", 1) +@mock.patch("aurweb.spawn.workers", 1) +@mock.patch("aurweb.spawn.children", [MockFakeProcess(1).process()]) +def test_spawn_stop_wait_failure(): + ctx = tempfile.TemporaryDirectory() + with ctx and mock.patch("aurweb.spawn.temporary_dir", ctx.name): + match = r"^Errors terminating the child processes" + with pytest.raises(aurweb.spawn.ProcessExceptions, match=match): + aurweb.spawn.stop() diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py new file mode 100644 index 00000000..68b6e7a0 --- /dev/null +++ b/test/test_ssh_pub_key.py @@ -0,0 +1,68 @@ +import pytest + +from aurweb import db +from aurweb.models.account_type import USER_ID +from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint +from aurweb.models.user import User + +TEST_SSH_PUBKEY = """ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCycoCi5yGCvSclH2wmNBUuwsYEzRZZBJaQquRc4y\ +sl+Tg+/jiDkR3Zn9fIznC4KnFoyrIHzkKuePZ3bNDYwkZxkJKoWBCh4hXKDXSm87FMN0+VDC+1QxF/\ +z0XaAGr/P6f4XukabyddypBdnHcZiplbw+YOSqcAE2TCqOlSXwNMOcF9U89UsR/Q9i9I52hlvU0q8+\ +fZVGhou1KCowFSnHYtrr5KYJ04CXkJ13DkVf3+pjQWyrByvBcf1hGEaczlgfobrrv/y96jDhgfXucx\ +liNKLdufDPPkii3LhhsNcDmmI1VZ3v0irKvd9WZuauqloobY84zEFcDTyjn0hxGjVeYFejm4fBnvjg\ +a0yZXORuWksdNfXWLDxFk6MDDd1jF0ExRbP+OxDuU4IVyIuDL7S3cnbf2YjGhkms/8voYT2OBE7FwN\ +lfv98Kr0NUp51zpf55Arxn9j0Rz9xTA7FiODQgCn6iQ0SDtzUNL0IKTCw26xJY5gzMxbfpvzPQGeul\ +x/ioM= kevr@volcano +""" + + +@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=USER_ID) + yield user + + +@pytest.fixture +def pubkey(user: User) -> SSHPubKey: + with db.begin(): + pubkey = db.create(SSHPubKey, User=user, + Fingerprint="testFingerprint", + PubKey="testPubKey") + yield pubkey + + +def test_pubkey(user: User, pubkey: SSHPubKey): + assert pubkey.UserID == user.ID + assert pubkey.User == user + assert pubkey.Fingerprint == "testFingerprint" + assert pubkey.PubKey == "testPubKey" + + +def test_pubkey_cs(user: User): + """ Test case sensitivity of the database table. """ + with db.begin(): + pubkey_cs = db.create(SSHPubKey, User=user, + Fingerprint="TESTFINGERPRINT", + PubKey="TESTPUBKEY") + + assert pubkey_cs.Fingerprint == "TESTFINGERPRINT" + assert pubkey_cs.Fingerprint != "testFingerprint" + assert pubkey_cs.PubKey == "TESTPUBKEY" + assert pubkey_cs.PubKey != "testPubKey" + + +def test_pubkey_fingerprint(): + assert get_fingerprint(TEST_SSH_PUBKEY) is not None + + +def test_pubkey_invalid_fingerprint(): + assert get_fingerprint("ssh-rsa fake and invalid") is None diff --git a/test/test_templates.py b/test/test_templates.py new file mode 100644 index 00000000..0d36d0b9 --- /dev/null +++ b/test/test_templates.py @@ -0,0 +1,328 @@ +import re + +from typing import Any, Dict + +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.models import Package, PackageBase, User +from aurweb.models.account_type import USER_ID +from aurweb.models.license import License +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.testing.html import parse_root +from aurweb.testing.requests import Request + +GIT_CLONE_URI_ANON = "anon_%s" +GIT_CLONE_URI_PRIV = "priv_%s" + + +@register_filter("func") +def func(): + pass + + +@register_function("function") +def function(): + pass + + +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) + return user + + +def create_pkgrel(package: Package, reltype_id: int, relname: str) \ + -> PackageRelation: + return db.create(PackageRelation, + Package=package, + RelTypeID=reltype_id, + RelName=relname) + + +@pytest.fixture +def user(db_test) -> User: + user = create_user("test") + yield user + + +@pytest.fixture +def pkgbase(user: User) -> PackageBase: + now = time.utcnow() + with db.begin(): + pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=user, + SubmittedTS=now, ModifiedTS=now) + yield pkgbase + + +@pytest.fixture +def package(user: User, pkgbase: PackageBase) -> Package: + with db.begin(): + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + yield pkg + + +def create_license(pkg: Package, license_name: str) -> PackageLicense: + lic = db.create(License, Name=license_name) + pkglic = db.create(PackageLicense, License=lic, Package=pkg) + return pkglic + + +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. """ + with pytest.raises(KeyError): + @register_function("function") + def some_func(): + pass + + +def test_commit_hash(): + # Hashes we'll use for this test. long_commit_hash should be + # shortened to commit_hash for rendering. + commit_hash = "abcdefg" + long_commit_hash = commit_hash + "1234567" + + 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) + + # Fake config.get_with_fallback. + config.original_get_with_fallback = config.get_with_fallback + config.get_with_fallback = config_get_with_fallback + + request = Request() + context = templates.make_context(request, "Test Context") + render = templates.render_raw_template(request, "index.html", context) + + # We've faked config.get_with_fallback to return a "valid" commit_hash + # when queried. Test that the expected render occurs. + commit_url = config.get("devel", "commit_url") + expected = commit_url % commit_hash + assert expected in render + assert f"HEAD@{commit_hash}" in render + assert long_commit_hash not in render + + # Restore config.get_with_fallback. + config.get_with_fallback = config.original_get_with_fallback + config.original_get_with_fallback = None + + # Now, we no longer fake the commit_hash option: no commit + # is displayed in the footer. Assert this expectation. + context = templates.make_context(request, "Test Context") + render = templates.render_raw_template(request, "index.html", context) + assert commit_hash not in render + + +def pager_context(num_packages: int) -> Dict[str, Any]: + return { + "request": Request(), + "singular": "%d package found.", + "plural": "%d packages found.", + "prefix": "/packages", + "total": num_packages, + "O": 0, + "PP": 50 + } + + +def test_pager_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) + + root = parse_root(body) + stats = root.xpath('//div[@class="pkglist-stats"]/p') + expected = "0 packages found." + assert stats[0].text.strip() == expected + + +def test_pager(): + """ 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) + + root = parse_root(body) + stats = root.xpath('//div[@class="pkglist-stats"]/p') + stats = re.sub(r"\s{2,}", " ", stats[0].text.strip()) + expected = f"{num_packages} packages found. Page 1 of 2." + assert stats == expected + + +def check_package_details(content: str, pkg: Package) -> None: + """ + Perform assertion checks against package details. + """ + pkgbase = pkg.PackageBase + + root = parse_root(content) + pkginfo = root.xpath('//table[@id="pkginfo"]')[0] + rows = pkginfo.xpath("./tr") + + # Check Git Clone URL. + git_clone_uris = rows[0].xpath("./td/a") + anon_uri, priv_uri = git_clone_uris + pkgbasename = pkgbase.Name + assert anon_uri.text.strip() == GIT_CLONE_URI_ANON % pkgbasename + assert priv_uri.text.strip() == GIT_CLONE_URI_PRIV % pkgbasename + + # Check Package Base. + pkgbase_markup = rows[1].xpath("./td/a")[0] + assert pkgbase_markup.text.strip() == pkgbasename + + # Check Description. + desc = rows[2].xpath("./td")[0] + assert desc.text.strip() == str(pkg.Description) + + # Check URL, for which we have none. In this case, no should + # be used since we have nothing to link. + url = rows[3].xpath("./td")[0] + assert url.text.strip() == str(pkg.URL) + + # Check Keywords, which should be empty. + keywords = rows[4].xpath("./td/form/div/input")[0] + assert keywords.attrib["value"] == str() + + i = 4 + licenses = pkg.package_licenses.all() + if licenses: + i += 1 + expected = ", ".join([p.License.Name for p in licenses]) + license_markup = rows[i].xpath("./td")[0] + assert license_markup.text.strip() == expected + else: + assert "Licenses" not in content + + provides = pkg.package_relations.filter( + PackageRelation.RelTypeID == PROVIDES_ID + ).all() + if provides: + i += 1 + expected = ", ".join([p.RelName for p in provides]) + provides_markup = rows[i].xpath("./td")[0] + assert provides_markup.text.strip() == expected + else: + assert "Provides" not in content + + replaces = pkg.package_relations.filter( + PackageRelation.RelTypeID == REPLACES_ID + ).all() + if replaces: + i += 1 + expected = ", ".join([r.RelName for r in replaces]) + replaces_markup = rows[i].xpath("./td")[0] + assert replaces_markup.text.strip() == expected + else: + assert "Replaces" not in content + + # Check Submitter. + selector = "./td" if not pkg.PackageBase.Submitter else "./td/a" + i += 1 + submitter = rows[i].xpath(selector)[0] + assert submitter.text.strip() == str(pkg.PackageBase.Submitter) + + # Check Maintainer. + selector = "./td" if not pkg.PackageBase.Maintainer else "./td/a" + i += 1 + maintainer = rows[i].xpath(selector)[0] + assert maintainer.text.strip() == str(pkg.PackageBase.Maintainer) + + # Check Packager. + selector = "./td" if not pkg.PackageBase.Packager else "./td/a" + i += 1 + packager = rows[i].xpath(selector)[0] + assert packager.text.strip() == str(pkg.PackageBase.Packager) + + # Check Votes. + i += 1 + votes = rows[i].xpath("./td")[0] + assert votes.text.strip() == str(pkg.PackageBase.NumVotes) + + # Check Popularity; for this package, a number_format of 6 places is used. + i += 1 + pop = rows[i].xpath("./td")[0] + assert pop.text.strip() == number_format(0, 6) + + # Check First Submitted + date_fmt = "%Y-%m-%d %H:%M" + i += 1 + first_submitted = rows[i].xpath("./td")[0] + converted_dt = as_timezone(to_dt(pkg.PackageBase.SubmittedTS), "UTC") + expected = converted_dt.strftime(date_fmt) + assert first_submitted.text.strip() == expected + + # Check Last Updated. + i += 1 + last_updated = rows[i].xpath("./td")[0] + converted_dt = as_timezone(to_dt(pkg.PackageBase.ModifiedTS), "UTC") + expected = converted_dt.strftime(date_fmt) + assert last_updated.text.strip() == expected + + +def test_package_details(user: User, package: Package): + """ 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 + }) + + base = base_template("partials/packages/details.html") + body = base.render(context, show_package_details=True) + check_package_details(body, package) + + +def test_package_details_filled(user: User, package: Package): + """ Test package details with all fields populated. """ + + pkgbase = package.PackageBase + with db.begin(): + # Setup Submitter and Packager; Maintainer is already set to `user`. + pkgbase.Submitter = pkgbase.Packager = user + + # Create two licenses. + create_license(package, "TPL") # Testing Public License + create_license(package, "TPL2") # Testing Public License 2 + + # Add provides. + create_pkgrel(package, PROVIDES_ID, "test-provider") + + # Add replaces. + create_pkgrel(package, REPLACES_ID, "test-replacement") + + 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), + }) + + base = base_template("partials/packages/details.html") + body = base.render(context, show_package_details=True) + check_package_details(body, package) diff --git a/test/test_term.py b/test/test_term.py new file mode 100644 index 00000000..bfa73a76 --- /dev/null +++ b/test/test_term.py @@ -0,0 +1,31 @@ +import pytest + +from sqlalchemy.exc import IntegrityError + +from aurweb import db +from aurweb.models.term import Term + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +def test_term_creation(): + with db.begin(): + 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" + assert term.Revision == 1 + + +def test_term_null_description_raises_exception(): + with pytest.raises(IntegrityError): + Term(URL="https://fake_url.io") + + +def test_term_null_url_raises_exception(): + with pytest.raises(IntegrityError): + Term(Description="Term description") diff --git a/test/test_time.py b/test/test_time.py new file mode 100644 index 00000000..2134d217 --- /dev/null +++ b/test/test_time.py @@ -0,0 +1,33 @@ +import aurweb.config + +from aurweb.testing.requests import Request +from aurweb.time import get_request_timezone, tz_offset + + +def test_tz_offset_utc(): + offset = tz_offset("UTC") + assert offset == "+00:00" + + +def test_tz_offset_mst(): + offset = tz_offset("MST") + assert offset == "-07:00" + + +def test_request_timezone(): + request = Request() + tz = get_request_timezone(request) + assert tz == aurweb.config.get("options", "default_timezone") + + +def test_authenticated_request_timezone(): + # Modify a fake request to be authenticated with the + # America/Los_Angeles timezone. + request = Request() + request.user.authenticated = True + request.user.Timezone = "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" diff --git a/test/test_trusted_user_routes.py b/test/test_trusted_user_routes.py new file mode 100644 index 00000000..0e6ca9ce --- /dev/null +++ b/test/test_trusted_user_routes.py @@ -0,0 +1,867 @@ +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, AccountType +from aurweb.models.tu_vote import TUVote +from aurweb.models.tu_voteinfo import TUVoteInfo +from aurweb.models.user import User +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% + + +def parse_root(html): + parser = lxml.etree.HTMLParser(recover=True) + tree = lxml.etree.parse(StringIO(html), parser) + return tree.getroot() + + +def get_table(root, class_name): + table = root.xpath(f'//table[contains(@class, "{class_name}")]')[0] + return table + + +def get_table_rows(table): + tbody = table.xpath("./tbody")[0] + return tbody.xpath("./tr") + + +def get_pkglist_directions(table): + stats = table.getparent().xpath("./div[@class='pkglist-stats']")[0] + nav = stats.xpath("./p[@class='pkglist-nav']")[0] + return nav.xpath("./a") + + +def get_a(node): + return node.xpath('./a')[0].text.strip() + + +def get_span(node): + return node.xpath('./span')[0].text.strip() + + +def assert_current_vote_html(row, expected): + columns = row.xpath("./td") + proposal, start, end, user, voted = columns + p, s, e, u, v = expected # Column expectations. + assert re.match(p, get_a(proposal)) is not None + assert re.match(s, start.text) is not None + assert re.match(e, end.text) is not None + assert re.match(u, get_a(user)) is not None + assert re.match(v, get_span(voted)) is not None + + +def assert_past_vote_html(row, expected): + columns = row.xpath("./td") + proposal, start, end, user, yes, no, voted = columns # Real columns. + p, s, e, u, y, n, v = expected # Column expectations. + assert re.match(p, get_a(proposal)) is not None + assert re.match(s, start.text) is not None + assert re.match(e, end.text) is not None + assert re.match(u, get_a(user)) is not None + assert re.match(y, yes.text) is not None + assert re.match(n, no.text) is not None + assert re.match(v, get_span(voted)) is not None + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def client(): + from aurweb.asgi import app + yield TestClient(app=app) + + +@pytest.fixture +def tu_user(): + tu_type = db.query(AccountType, + AccountType.AccountType == "Trusted User").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 + + +@pytest.fixture +def user(): + 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) + yield user + + +@pytest.fixture +def proposal(user, tu_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) + + +def test_tu_index_guest(client): + headers = {"referer": config.get("options", "aur_location") + "/tu"} + with client as request: + response = request.get("/tu", allow_redirects=False, headers=headers) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + params = filters.urlencode({"next": "/tu"}) + assert response.headers.get("location") == f"/login?{params}" + + +def test_tu_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) + 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. """ + + # Make a default get request to /tu. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Parse lxml root. + root = parse_root(response.text) + + # Check that .current-votes does not exist. + tables = root.xpath('//table[contains(@class, "current-votes")]') + assert len(tables) == 0 + + # Check that .past-votes has does not exist. + tables = root.xpath('//table[contains(@class, "current-votes")]') + assert len(tables) == 0 + + +def test_tu_index(client, tu_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. + ] + 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)) + + 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) + + cookies = {"AURSID": tu_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) + + assert response.status_code == int(HTTPStatus.OK) + + # Rows we expect to exist in HTML produced by /tu for current votes. + expected_rows = [ + ( + r'Test agenda 1', + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^(Yes|No)$' + ) + ] + + # Assert that we are matching the number of current votes. + current_votes = [c for c in votes if c[2] > ts] + assert len(current_votes) == len(expected_rows) + + # Parse lxml.etree root. + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + for i, row in enumerate(rows): + assert_current_vote_html(row, expected_rows[i]) + + # Assert that we are matching the number of past votes. + 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. + expected_rows = [ + ( + r'Test agenda 2', + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^\d+$', + r'^\d+$', + r'^(Yes|No)$' + ) + ] + + table = get_table(root, "past-votes") + rows = get_table_rows(table) + for i, row in enumerate(rows): + assert_past_vote_html(row, expected_rows[i]) + + # Get the .last-votes table and check that our vote shows up. + table = get_table(root, "last-votes") + rows = get_table_rows(table) + assert len(rows) == 1 + + # Check to see the rows match up to our user and related vote. + username, vote_id = rows[0] + vote_id = vote_id.xpath("./a")[0] + assert username.text.strip() == tu_user.Username + assert int(vote_id.text.strip()) == vote_records[1].ID + + +def test_tu_index_table_paging(client, tu_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) + + 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) + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Parse lxml.etree root. + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + assert len(rows) == 10 + + def make_expectation(offset, i): + return [ + f"Agenda #{offset + i}", + DATETIME_REGEX, + DATETIME_REGEX, + tu_user.Username, + r'^(Yes|No)$' + ] + + for i, row in enumerate(rows): + assert_current_vote_html(row, make_expectation(0, i)) + + # Parse out Back/Next buttons. + directions = get_pkglist_directions(table) + assert len(directions) == 1 + assert "Next" in directions[0].text + + # 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) + assert response.status_code == int(HTTPStatus.OK) + + old_rows = rows + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + assert rows != old_rows + + for i, row in enumerate(rows): + assert_current_vote_html(row, make_expectation(offset, i)) + + # Parse out Back/Next buttons. + directions = get_pkglist_directions(table) + assert len(directions) == 2 + assert "Back" in directions[0].text + assert "Next" in directions[1].text + + # Make sure past-votes' Back/Next were not affected. + past_votes = get_table(root, "past-votes") + past_directions = get_pkglist_directions(past_votes) + assert len(past_directions) == 1 + assert "Next" in past_directions[0].text + + offset = 20 # Specify coff=10 + with client as request: + response = request.get("/tu", cookies=cookies, params={ + "coff": offset + }, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Do it again, we only have five left. + old_rows = rows + root = parse_root(response.text) + + table = get_table(root, "current-votes") + rows = get_table_rows(table) + assert rows != old_rows + for i, row in enumerate(rows): + assert_current_vote_html(row, make_expectation(offset, i)) + + # Parse out Back/Next buttons. + directions = get_pkglist_directions(table) + assert len(directions) == 1 + assert "Back" in directions[0].text + + # Make sure past-votes' Back/Next were not affected. + past_votes = get_table(root, "past-votes") + past_directions = get_pkglist_directions(past_votes) + assert len(past_directions) == 1 + assert "Next" in past_directions[0].text + + +def test_tu_index_sorting(client, tu_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) + + # 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")} + with client as request: + response = request.get("/tu", cookies=cookies, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + # Get lxml handles of the document. + root = parse_root(response.text) + table = get_table(root, "current-votes") + rows = get_table_rows(table) + + # The latest Agenda is at the top by default. + 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)$' + ]) + + # 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) + assert response.status_code == int(HTTPStatus.OK) + + # Get lxml handles of the document. + root = parse_root(response.text) + table = get_table(root, "current-votes") + rows = get_table_rows(table) + + # Reverse our expectations and assert that the proposals got flipped. + 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)$' + ]) + + +def test_tu_index_last_votes(client, tu_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, + ActiveTUs=1, + Quorum=0.0, + Submitter=tu_user) + + # Create a vote on it from tu_user. + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + + # Now, check that tu_user got populated in the .last-votes table. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + table = get_table(root, "last-votes") + rows = get_table_rows(table) + assert len(rows) == 1 + + last_vote = rows[0] + user, vote_id = last_vote.xpath("./td") + vote_id = vote_id.xpath("./a")[0] + + assert user.text.strip() == tu_user.Username + assert int(vote_id.text.strip()) == voteinfo.ID + + +def test_tu_proposal_not_found(client, tu_user): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/tu", params={"id": 1}, cookies=cookies) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_tu_proposal_unauthorized(client: TestClient, user: User, + proposal: Tuple[User, User, TUVoteInfo]): + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/tu/{proposal[2].ID}" + with client as request: + response = request.get(endpoint, cookies=cookies, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/tu" + + with client as request: + response = request.post(endpoint, cookies=cookies, + data={"decision": False}, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/tu" + + +def test_tu_running_proposal(client: TestClient, + proposal: Tuple[User, User, TUVoteInfo]): + tu_user, user, voteinfo = proposal + + # Initiate an authenticated GET request to /tu/{proposal_id}. + proposal_id = voteinfo.ID + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get(f"/tu/{proposal_id}", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Alright, now let's continue on to verifying some markup. + # First, let's verify that the proposal details match. + root = parse_root(response.text) + details = root.xpath('//div[@class="proposal details"]')[0] + + vote_running = root.xpath('//p[contains(@class, "vote-running")]')[0] + assert vote_running.text.strip() == "This vote is still running." + + # Verify User field. + 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 + 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}" + + 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 + + # We have not voted yet. Assert that our voting form is shown. + form = root.xpath('//form[contains(@class, "action-form")]')[0] + fields = form.xpath("./fieldset")[0] + buttons = fields.xpath('./button[@name="decision"]') + assert len(buttons) == 3 + + # Check the button names and values. + yes, no, abstain = buttons + + # Yes + assert yes.attrib["name"] == "decision" + assert yes.attrib["value"] == "Yes" + + # No + assert no.attrib["name"] == "decision" + assert no.attrib["value"] == "No" + + # Abstain + assert abstain.attrib["name"] == "decision" + assert abstain.attrib["value"] == "Abstain" + + # Create a vote. + with db.begin(): + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + voteinfo.ActiveTUs += 1 + voteinfo.Yes += 1 + + # Make another request now that we've voted. + with client as request: + response = request.get( + "/tu", params={"id": voteinfo.ID}, cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Parse our new root. + root = parse_root(response.text) + + # Check that we no longer have a voting form. + form = root.xpath('//form[contains(@class, "action-form")]') + assert not form + + # Check that we're told we've voted. + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You've already voted for this proposal." + + +def test_tu_ended_proposal(client, proposal): + tu_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}. + proposal_id = voteinfo.ID + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get(f"/tu/{proposal_id}", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + # Alright, now let's continue on to verifying some markup. + # First, let's verify that the proposal details match. + root = parse_root(response.text) + details = root.xpath('//div[@class="proposal details"]')[0] + + vote_running = root.xpath('//p[contains(@class, "vote-running")]') + assert not vote_running + + result_node = details.xpath('./div[contains(@class, "result")]')[0] + result_label = result_node.xpath("./text()")[0] + assert result_label.strip() == "Result:" + + result = result_node.xpath("./span/text()")[0] + assert result.strip() == "unknown" + + # Check that voting has ended. + form = root.xpath('//form[contains(@class, "action-form")]') + assert not form + + # We should see a status about it. + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + 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")} + with client as request: + data = {"decision": "Yes"} + response = request.post("/tu/1", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + +def test_tu_proposal_vote(client, proposal): + tu_user, user, voteinfo = proposal + + # Store the current related values. + yes = voteinfo.Yes + active_tus = voteinfo.ActiveTUs + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "Yes"} + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data) + assert response.status_code == int(HTTPStatus.OK) + + # Check that the proposal record got updated. + assert voteinfo.Yes == yes + 1 + assert voteinfo.ActiveTUs == active_tus + 1 + + # Check that the new TUVote exists. + vote = db.query(TUVote, TUVote.VoteInfo == voteinfo, + TUVote.User == tu_user).first() + assert vote is not None + + root = parse_root(response.text) + + # Check that we're told we've voted. + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + 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 + + with db.begin(): + tu_user.AccountTypeID = DEVELOPER_ID + + cookies = {"AURSID": tu_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) + 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." + + with client as request: + data = {"decision": "Yes"} + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + 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." + + +def test_tu_proposal_vote_cant_self_vote(client, proposal): + tu_user, user, voteinfo = proposal + + # Update voteinfo.User. + with db.begin(): + voteinfo.User = tu_user.Username + + cookies = {"AURSID": tu_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) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You cannot vote in an proposal about you." + + with client as request: + data = {"decision": "Yes"} + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You cannot vote in an proposal about you." + + +def test_tu_proposal_vote_already_voted(client, proposal): + tu_user, user, voteinfo = proposal + + with db.begin(): + db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + voteinfo.Yes += 1 + voteinfo.ActiveTUs += 1 + + cookies = {"AURSID": tu_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) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You've already voted for this proposal." + + with client as request: + data = {"decision": "Yes"} + response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, + data=data, allow_redirects=False) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + status = root.xpath('//span[contains(@class, "status")]/text()')[0] + assert status == "You've already voted for this proposal." + + +def test_tu_proposal_vote_invalid_decision(client, proposal): + tu_user, user, voteinfo = proposal + + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + data = {"decision": "EVIL"} + response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, + 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")} + with client as request: + response = request.get("/addvote", cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + +def test_tu_addvote_unauthorized(client: TestClient, user: User, + proposal: Tuple[User, User, TUVoteInfo]): + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + response = request.get("/addvote", cookies=cookies, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/tu" + + with client as request: + response = request.post("/addvote", cookies=cookies, + allow_redirects=False) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + assert response.headers.get("location") == "/tu" + + +def test_tu_addvote_invalid_type(client: TestClient, tu_user: User): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + with client as request: + response = request.get("/addvote", params={"type": "faketype"}, + cookies=cookies) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + error = root.xpath('//*[contains(@class, "error")]/text()')[0] + assert error.strip() == "Invalid type." + + +def test_tu_addvote_post(client: TestClient, tu_user: User, user: User): + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + + data = { + "user": user.Username, + "type": "add_tu", + "agenda": "Blah" + } + + with client as request: + response = request.post("/addvote", cookies=cookies, data=data) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + voteinfo = db.query(TUVoteInfo, TUVoteInfo.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")} + + data = { + "user": user.Username, + "type": "add_tu", + "agenda": "Blah" + } + + with client as request: + response = request.post("/addvote", cookies=cookies, data=data) + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + voteinfo = db.query(TUVoteInfo, TUVoteInfo.Agenda == "Blah").first() + assert voteinfo is not None + + with client as request: + response = request.post("/addvote", cookies=cookies, 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")} + data = {"user": "fakeusername"} + with client as request: + response = request.post("/addvote", cookies=cookies, 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")} + data = {"user": user.Username} + with client as request: + response = request.post("/addvote", cookies=cookies, 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"} + with client as request: + response = request.post("/addvote", cookies=cookies, data=data) + assert response.status_code == int(HTTPStatus.BAD_REQUEST) + + +def test_tu_addvote_post_bylaws(client: TestClient, tu_user: User): + # Bylaws votes do not need a user specified. + cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + data = {"type": "bylaws", "agenda": "Blah blah!"} + with client as request: + response = request.post("/addvote", cookies=cookies, data=data) + assert response.status_code == int(HTTPStatus.SEE_OTHER) diff --git a/test/test_tu_vote.py b/test/test_tu_vote.py new file mode 100644 index 00000000..91d73ecb --- /dev/null +++ b/test/test_tu_vote.py @@ -0,0 +1,54 @@ +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 new file mode 100644 index 00000000..17226048 --- /dev/null +++ b/test/test_tu_voteinfo.py @@ -0,0 +1,148 @@ +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 new file mode 100644 index 00000000..a54c52a4 --- /dev/null +++ b/test/test_tuvotereminder.py @@ -0,0 +1,101 @@ +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 new file mode 100644 index 00000000..7871cd61 --- /dev/null +++ b/test/test_user.py @@ -0,0 +1,316 @@ +import hashlib +import json + +from datetime import datetime, timedelta + +import bcrypt +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.ban import Ban +from aurweb.models.package import Package +from aurweb.models.package_base import PackageBase +from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_vote import PackageVote +from aurweb.models.session import Session +from aurweb.models.ssh_pub_key import SSHPubKey +from aurweb.models.user import User +from aurweb.testing.requests import Request + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +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) + return user + + +@pytest.fixture +def user() -> User: + user = create_user("test", USER_ID) + yield user + + +@pytest.fixture +def tu_user() -> User: + user = create_user("test_tu", TRUSTED_USER_ID) + yield user + + +@pytest.fixture +def dev_user() -> User: + user = create_user("test_dev", DEVELOPER_ID) + yield user + + +@pytest.fixture +def tu_and_dev_user() -> User: + user = create_user("test_tu_and_dev", TRUSTED_USER_AND_DEV_ID) + yield user + + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase = db.create(PackageBase, Name="pkg1", Maintainer=user) + pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + yield pkg + + +def test_user_login_logout(user: User): + """ Test creating a user and reading its columns. """ + # Assert that make_user created a valid user. + assert bool(user.ID) + + # Test authentication. + assert user.valid_password("testPassword") + assert not user.valid_password("badPassword") + + # Make a raw request. + request = Request() + assert not user.login(request, "badPassword") + assert not user.is_authenticated() + + sid = user.login(request, "testPassword") + assert sid is not None + assert user.is_authenticated() + + # Expect that User session relationships work right. + 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 + + # Search for the user via query API. + result = db.query(User, User.ID == user.ID).first() + + # Compare the result and our original user. + assert result == user + assert result.ID == user.ID + assert result.AccountType.ID == user.AccountType.ID + assert result.Username == user.Username + assert result.Email == user.Email + + # Test result authenticate methods to ensure they work the same. + assert not result.valid_password("badPassword") + assert result.valid_password("testPassword") + assert result.is_authenticated() + + # Test out user string functions. + assert repr(user) == f"" + + # Test logout. + user.logout(request) + assert not user.is_authenticated() + + +def test_user_login_twice(user: User): + request = Request() + assert user.login(request, "testPassword") + assert user.login(request, "testPassword") + + +def test_user_login_banned(user: User): + # Add ban for the next 30 seconds. + banned_timestamp = datetime.utcnow() + timedelta(seconds=30) + with db.begin(): + db.create(Ban, IPAddress="127.0.0.1", BanTS=banned_timestamp) + + request = Request() + request.client.host = "127.0.0.1" + assert not user.login(request, "testPassword") + + +def test_user_login_suspended(user: User): + with db.begin(): + user.Suspended = True + assert not user.login(Request(), "testPassword") + + +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() + + assert not user.valid_password("badPassword") + assert user.valid_password("testPassword") + + # Test by passing a password of None value in. + assert not user.valid_password(None) + + +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) + sid = user.login(Request(), "testPassword") + assert sid and user.is_authenticated() + assert sid != "stub" + + +def test_user_update_password(user: User): + user.update_password("secondPassword") + assert not user.valid_password("testPassword") + assert user.valid_password("secondPassword") + + +def test_user_minimum_passwd_length(): + passwd_min_len = aurweb.config.getint("options", "passwd_min_len") + assert User.minimum_passwd_length() == passwd_min_len + + +def test_user_has_credential(user: User): + assert not user.has_credential(creds.ACCOUNT_CHANGE_TYPE) + + +def test_user_ssh_pub_key(user: User): + assert user.ssh_pub_key is None + + with db.begin(): + ssh_pub_key = db.create(SSHPubKey, UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey") + + assert user.ssh_pub_key == 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 not in creds.developer + assert user.AccountTypeID not in creds.trusted_user_or_dev + + with db.begin(): + user.AccountTypeID = at.TRUSTED_USER_ID + + assert user.AccountTypeID in creds.trusted_user + assert user.AccountTypeID in creds.trusted_user_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 + + with db.begin(): + user.AccountTypeID = at.TRUSTED_USER_AND_DEV_ID + + assert user.AccountTypeID in creds.trusted_user + assert user.AccountTypeID in creds.developer + assert user.AccountTypeID in creds.trusted_user_or_dev + + # Some model authorization checks. + assert user.is_elevated() + assert user.is_trusted_user() + assert user.is_developer() + + +def test_user_json(user: User): + data = json.loads(user.json()) + assert data.get("ID") == user.ID + assert data.get("Username") == user.Username + assert data.get("Email") == user.Email + # .json() converts datetime values to integer timestamps. + assert isinstance(data.get("RegistrationTS"), int) + + +def test_user_as_dict(user: User): + data = user.as_dict() + assert data.get("ID") == user.ID + assert data.get("Username") == user.Username + assert data.get("Email") == user.Email + # .as_dict() does not convert values to json-capable types. + assert isinstance(data.get("RegistrationTS"), datetime) + + +def test_user_is_trusted_user(user: User): + with db.begin(): + user.AccountTypeID = at.TRUSTED_USER_ID + assert user.is_trusted_user() 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 + + +def test_user_is_developer(user: User): + with db.begin(): + user.AccountTypeID = at.DEVELOPER_ID + assert user.is_developer() is True + + # Do it again with the combined role. + with db.begin(): + user.AccountTypeID = at.TRUSTED_USER_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()) + with db.begin(): + db.create(PackageVote, PackageBase=pkgbase, User=user, VoteTS=now) + assert user.voted_for(package) + + +def test_user_notified(user: User, package: Package): + pkgbase = package.PackageBase + with db.begin(): + db.create(PackageNotification, PackageBase=pkgbase, User=user) + assert user.notified(package) + + +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): + # 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(dev_user) + assert not user.can_edit_user(tu_and_dev_user) + + # Trusted User can edit. + assert tu_user.can_edit_user(user) + assert tu_user.can_edit_user(tu_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) + + # Developer can edit. + assert dev_user.can_edit_user(user) + assert dev_user.can_edit_user(tu_user) + assert dev_user.can_edit_user(dev_user) + + # Developer cannot edit. + assert not dev_user.can_edit_user(tu_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) diff --git a/test/test_usermaint.py b/test/test_usermaint.py new file mode 100644 index 00000000..e572569a --- /dev/null +++ b/test/test_usermaint.py @@ -0,0 +1,65 @@ +import pytest + +from aurweb import db, time +from aurweb.models import User +from aurweb.models.account_type import USER_ID +from aurweb.scripts import usermaint + + +@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", + 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. """ + + now = time.utcnow() + with db.begin(): + user.LastLoginIPAddress = "127.0.0.1" + user.LastLogin = now - 10 + user.LastSSHLoginIPAddress = "127.0.0.1" + user.LastSSHLogin = now - 10 + + usermaint.main() + + assert user.LastLoginIPAddress == "127.0.0.1" + assert user.LastSSHLoginIPAddress == "127.0.0.1" + + +def test_usermaint(user: User): + """ + In this case, we first test that only the expired record gets + updated, but the non-expired record remains untouched. After, + we update the login time on the non-expired record and exercise + its code path. + """ + + now = time.utcnow() + limit_to = now - 86400 * 7 + with db.begin(): + user.LastLoginIPAddress = "127.0.0.1" + user.LastLogin = limit_to - 666 + user.LastSSHLoginIPAddress = "127.0.0.1" + user.LastSSHLogin = now - 10 + + usermaint.main() + + assert user.LastLoginIPAddress is None + assert user.LastSSHLoginIPAddress == "127.0.0.1" + + with db.begin(): + user.LastSSHLogin = limit_to - 666 + + usermaint.main() + + assert user.LastLoginIPAddress is None + assert user.LastSSHLoginIPAddress is None diff --git a/test/test_util.py b/test/test_util.py new file mode 100644 index 00000000..51d978fb --- /dev/null +++ b/test/test_util.py @@ -0,0 +1,62 @@ +import json + +from http import HTTPStatus + +import fastapi +import pytest + +from fastapi.responses import JSONResponse + +from aurweb import filters, util +from aurweb.testing.requests import Request + + +def test_round(): + assert filters.do_round(1.3) == 1 + assert filters.do_round(1.5) == 2 + assert filters.do_round(2.0) == 2 + + +def test_git_search(): + """ Test that git_search matches the full commit if necessary. """ + commit_hash = "0123456789abcdef" + repo = {commit_hash} + prefixlen = util.git_search(repo, commit_hash) + assert prefixlen == 16 + + +def test_git_search_double_commit(): + """ 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. + prefixlen = util.git_search(repo, commit_hash) + assert prefixlen == 13 + + +@pytest.mark.asyncio +async def test_error_or_result(): + + async def route(request: fastapi.Request): + raise RuntimeError("No response returned.") + + response = await util.error_or_result(route, Request()) + assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR + + data = json.loads(response.body) + assert data.get("error") == "No response returned." + + async def good_route(request: fastapi.Request): + return JSONResponse() + + response = await util.error_or_result(good_route, Request()) + assert response.status_code == HTTPStatus.OK + + +def test_valid_homepage(): + assert util.valid_homepage("http://google.com") + assert util.valid_homepage("https://google.com") + assert not util.valid_homepage("http://[google.com/broken-ipv6") + assert not util.valid_homepage("https://[google.com/broken-ipv6") + + assert not util.valid_homepage("gopher://gopher.hprc.utoronto.ca/") diff --git a/util/fix-coverage b/util/fix-coverage new file mode 100755 index 00000000..3446c4af --- /dev/null +++ b/util/fix-coverage @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +""" A simple script which updates paths for .coverage executed +in another directory. This is particularly useful for fixing +.coverage files received via ./cache/.coverage after Docker runs. + +Copyright (C) 2021 aurweb Development +All Rights Reserved. +""" +import os +import re +import sqlite3 +import sys +import traceback + +import aurweb.config + + +def eprint(*args, **kwargs): + print(*args, **kwargs, file=sys.stderr) + + +def main(): + if len(sys.argv) == 1: + print(f"usage: {sys.argv[0]} .coverage") + return 1 + + coverage_db = sys.argv[1] + if not os.path.exists(coverage_db): + eprint("error: coverage file not found") + return 2 + + aurwebdir = aurweb.config.get("options", "aurwebdir") + new_coverage_db = os.path.join(aurwebdir, ".coverage") + with open(coverage_db, "rb") as i: + with open(new_coverage_db, "wb") as f: + f.write(i.read()) + print(f"Copied coverage db to {new_coverage_db}.") + coverage_db = new_coverage_db + + sqlite3.paramstyle = "format" + db = sqlite3.connect(coverage_db) + + cursor = db.cursor() + results = cursor.execute("SELECT * FROM file") + + files = dict() + for i, path in results: + 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)) + + db.commit() + db.close() + + return 0 + + +if __name__ == "__main__": + e = 1 + try: + e = main() + except Exception: + traceback.print_exc() + e = 1 + sys.exit(e) diff --git a/util/sendmail b/util/sendmail new file mode 100755 index 00000000..9356851a --- /dev/null +++ b/util/sendmail @@ -0,0 +1,25 @@ +#!/bin/bash +# Send email to temporary filesystem for tests. +dir='test-emails' +filename='email.txt' +if [ ! -z ${PYTEST_CURRENT_TEST+x} ]; then + filename="$(echo $PYTEST_CURRENT_TEST | cut -d ' ' -f 1 | sed -r 's/(\/|\.|,|:)/_/g')" +fi +mkdir -p "$dir" + +path="${dir}/${filename}" +serial_file="${path}.serial" +if [ ! -f $serial_file ]; then + echo 0 > $serial_file +fi + +# Increment and update $serial_file. +serial=$(($(cat $serial_file) + 1)) +echo $serial > $serial_file + +# Use the serial we're on to mark the email file. +# Emails have the format: PYTEST_CURRENT_TEST.s.txt +# where s is the current serial for PYTEST_CURRENT_TEST. +cat > "${path}.${serial}.txt" + +exit 0 diff --git a/web/html/addvote.php b/web/html/addvote.php index 3672c031..83b800b8 100644 --- a/web/html/addvote.php +++ b/web/html/addvote.php @@ -67,8 +67,10 @@ if (has_credential(CRED_TU_ADD_VOTE)) { } } - if (!empty($_POST['addVote']) && empty($error)) { - add_tu_proposal($_POST['agenda'], $_POST['user'], $len, $quorum, $uid); + if (!empty($_POST['addVote']) && empty($error)) { + // Convert $quorum to a String of maximum length "12.34" (5). + add_tu_proposal($_POST['agenda'], $_POST['user'], + $len, strval($quorum), $uid); print "

" . __("New proposal submitted.") . "

\n"; } else { diff --git a/web/html/css/archweb.css b/web/html/css/archweb.css index f95e3843..45b9bff0 100644 --- a/web/html/css/archweb.css +++ b/web/html/css/archweb.css @@ -329,6 +329,7 @@ label { input[type=text], input[type=password], +input[type=email], textarea { padding: 0.10em; } @@ -556,6 +557,10 @@ h3 span.arrow { margin: -2em 0 0 0; } + #pkg-updates .rss-icon.latest { + margin-right: 1em; + } + #pkg-updates table { margin: 0; } diff --git a/web/html/css/aurweb.css b/web/html/css/aurweb.css index 81bf9ab6..d5752c07 100644 --- a/web/html/css/aurweb.css +++ b/web/html/css/aurweb.css @@ -144,8 +144,7 @@ span.hover-help { cursor:help; } -label.confirmation, -#merge-into { +label.confirmation { width: auto; } @@ -199,3 +198,68 @@ label.confirmation, .error { color: red; } + +.article-content > div { + overflow: hidden; + transition: height 1s; +} + +.proposal.details { + margin: .33em 0 1em; +} + +button[type="submit"], +button[type="reset"] { + padding: 0 0.6em; +} + +.results tr td[align="left"] fieldset { + text-align: left; +} + +.results tr td[align="right"] fieldset { + text-align: right; +} + +input#search-action-submit { + width: 80px; +} + +.success { + color: green; +} + +/* Styling used to clone
styles for a form.link button. */ +form.link, form.link button { + display: inline; + font-family: sans-serif; +} +form.link button { + padding: 0 0.5em; + color: #07b; + background: none; + border: none; + font-family: inherit; + font-size: inherit; +} +form.link button:hover { + cursor: pointer; + text-decoration: underline; +} + +/* Customize form.link when used inside of a page. */ +div.box form.link p { + margin: .33em 0 1em; +} +div.box form.link button { + padding: 0; +} + +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; +} diff --git a/web/html/home.php b/web/html/home.php index 0ce89f40..5ea79ee9 100644 --- a/web/html/home.php +++ b/web/html/home.php @@ -74,9 +74,9 @@ if (isset($_COOKIE["AURSID"])) { ', + '', '', - '', + '', '' ); ?> @@ -84,7 +84,7 @@ if (isset($_COOKIE["AURSID"])) { echo __( 'Contributed PKGBUILDs %smust%s conform to the %sArch Packaging Standards%s otherwise they will be deleted!', '', '', - '', + '', '' ); ?> @@ -95,7 +95,7 @@ if (isset($_COOKIE["AURSID"])) { :

-

+

@@ -131,7 +131,7 @@ if (isset($_COOKIE["AURSID"])) { ', + '', '' ); ?> @@ -185,7 +185,7 @@ if (isset($_COOKIE["AURSID"])) {
- " maxlength="35" /> + " maxlength="35" autocomplete="off"/>
@@ -202,34 +202,13 @@ if (isset($_COOKIE["AURSID"])) { - - - + "+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),(e.browser.chrome||e.browser.webkit||e.browser.msie)&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this))},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=!~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(e){var t=this;setTimeout(function(){t.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(t){this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")}},e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e(function(){e("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;t.preventDefault(),n.typeahead(n.data())})})}(window.jQuery) \ No newline at end of file diff --git a/web/html/js/comment-edit.js b/web/html/js/comment-edit.js new file mode 100644 index 00000000..23ffdd34 --- /dev/null +++ b/web/html/js/comment-edit.js @@ -0,0 +1,61 @@ +function add_busy_indicator(sibling) { + const img = document.createElement('img'); + img.src = "/static/images/ajax-loader.gif"; + img.classList.add('ajax-loader'); + img.style.height = 11; + img.style.width = 16; + img.alt = "Busy…"; + + sibling.insertAdjacentElement('afterend', img); +} + +function remove_busy_indicator(sibling) { + const elem = sibling.nextElementSibling; + elem.parentNode.removeChild(elem); +} + +function getParentsUntil(elem, className) { + // Limit to 10 depth + for ( ; elem && elem !== document; elem = elem.parentNode) { + if (elem.matches(className)) { + break; + } + } + + return elem; +} + +function handleEditCommentClick(event, pkgbasename) { + event.preventDefault(); + const parent_element = getParentsUntil(event.target, '.comment-header'); + const parent_id = parent_element.id; + const comment_id = parent_id.substr(parent_id.indexOf('-') + 1); + // The div class="article-content" which contains the comment + const edit_form = parent_element.nextElementSibling; + + const url = "/pkgbase/" + pkgbasename + "/comments/" + comment_id + "/form?"; + + add_busy_indicator(event.target); + + fetch(url + new URLSearchParams({ next: window.location.pathname }), { + method: 'GET', + credentials: 'same-origin' + }) + .then(function(response) { + if (!response.ok) { + throw Error(response.statusText); + } + return response.json(); + }) + .then(function(data) { + remove_busy_indicator(event.target); + edit_form.innerHTML = data.form; + edit_form.querySelector('textarea').focus(); + }) + .catch(function(error) { + remove_busy_indicator(event.target); + console.error(error); + }); + + return false; +} diff --git a/web/html/js/copy.js b/web/html/js/copy.js new file mode 100644 index 00000000..f46299b3 --- /dev/null +++ b/web/html/js/copy.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + document.querySelector('.copy').addEventListener('click', function(e) { + e.preventDefault(); + navigator.clipboard.writeText(event.target.text); + }); +}); diff --git a/web/html/js/typeahead-home.js b/web/html/js/typeahead-home.js new file mode 100644 index 00000000..5af51c53 --- /dev/null +++ b/web/html/js/typeahead-home.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + const input = document.getElementById('pkgsearch-field'); + const form = document.getElementById('pkgsearch-form'); + const type = 'suggest'; + typeahead.init(type, input, form); +}); diff --git a/web/html/js/typeahead-pkgbase-merge.js b/web/html/js/typeahead-pkgbase-merge.js new file mode 100644 index 00000000..a8c87e4f --- /dev/null +++ b/web/html/js/typeahead-pkgbase-merge.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + const input = document.getElementById('merge_into'); + const form = document.getElementById('merge-form'); + const type = "suggest-pkgbase"; + typeahead.init(type, input, form, false); +}); diff --git a/web/html/js/typeahead-pkgbase-request.js b/web/html/js/typeahead-pkgbase-request.js new file mode 100644 index 00000000..e012d55f --- /dev/null +++ b/web/html/js/typeahead-pkgbase-request.js @@ -0,0 +1,36 @@ +function showHideMergeSection() { + const elem = document.getElementById('id_type'); + const merge_section = document.getElementById('merge_section'); + if (elem.value == 'merge') { + merge_section.style.display = ''; + } else { + merge_section.style.display = 'none'; + } +} + +function showHideRequestHints() { + document.getElementById('deletion_hint').style.display = 'none'; + document.getElementById('merge_hint').style.display = 'none'; + document.getElementById('orphan_hint').style.display = 'none'; + + const elem = document.getElementById('id_type'); + document.getElementById(elem.value + '_hint').style.display = ''; +} + +document.addEventListener('DOMContentLoaded', function() { + showHideMergeSection(); + showHideRequestHints(); + + const input = document.getElementById('id_merge_into'); + const form = document.getElementById('request-form'); + const type = "suggest-pkgbase"; + + typeahead.init(type, input, form, false); +}); + +// Bind the change event here, otherwise we have to inline javascript, +// which angers CSP (Content Security Policy). +document.getElementById("id_type").addEventListener("change", function() { + showHideMergeSection(); + showHideRequestHints(); +}); diff --git a/web/html/js/typeahead.js b/web/html/js/typeahead.js new file mode 100644 index 00000000..bfd3d156 --- /dev/null +++ b/web/html/js/typeahead.js @@ -0,0 +1,151 @@ +"use strict"; + +const typeahead = (function() { + var input; + var form; + var suggest_type; + var list; + var submit = true; + + function resetResults() { + if (!list) return; + list.style.display = "none"; + list.innerHTML = ""; + } + + function getCompleteList() { + if (!list) { + list = document.createElement("UL"); + list.setAttribute("class", "pkgsearch-typeahead"); + form.appendChild(list); + setListLocation(); + } + return list; + } + + function onListClick(e) { + let target = e.target; + while (!target.getAttribute('data-value')) { + target = target.parentNode; + } + input.value = target.getAttribute('data-value'); + if (submit) { + form.submit(); + } + } + + function setListLocation() { + if (!list) return; + const rects = input.getClientRects()[0]; + list.style.top = (rects.top + rects.height) + "px"; + list.style.left = rects.left + "px"; + } + + function loadData(letter, data) { + const pkgs = data.slice(0, 10); // Show maximum of 10 results + + resetResults(); + + if (pkgs.length === 0) { + return; + } + + const ul = getCompleteList(); + ul.style.display = "block"; + const fragment = document.createDocumentFragment(); + + for (let i = 0; i < pkgs.length; i++) { + const item = document.createElement("li"); + const text = pkgs[i].replace(letter, '' + letter + ''); + item.innerHTML = '' + text + ''; + item.setAttribute('data-value', pkgs[i]); + fragment.appendChild(item); + } + + ul.appendChild(fragment); + ul.addEventListener('click', onListClick); + } + + function fetchData(letter) { + const url = '/rpc?v=5&type=' + suggest_type + '&arg=' + letter; + fetch(url).then(function(response) { + return response.json(); + }).then(function(data) { + loadData(letter, data); + }); + } + + function onInputClick() { + if (input.value === "") { + resetResults(); + return; + } + fetchData(input.value); + } + + function onKeyDown(e) { + if (!list) return; + + const elem = document.querySelector(".pkgsearch-typeahead li.active"); + switch(e.keyCode) { + case 13: // enter + if (!submit) { + return; + } + if (elem) { + input.value = elem.getAttribute('data-value'); + form.submit(); + } else { + form.submit(); + } + e.preventDefault(); + break; + case 38: // up + if (elem && elem.previousElementSibling) { + elem.className = ""; + elem.previousElementSibling.className = "active"; + } + e.preventDefault(); + break; + case 40: // down + if (elem && elem.nextElementSibling) { + elem.className = ""; + elem.nextElementSibling.className = "active"; + } else if (!elem && list.childElementCount !== 0) { + list.children[0].className = "active"; + } + e.preventDefault(); + break; + } + } + + // debounce https://davidwalsh.name/javascript-debounce-function + function debounce(func, wait, immediate) { + var timeout; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + }; + } + + return { + init: function(type, inputfield, formfield, submitdata = true) { + suggest_type = type; + input = inputfield; + form = formfield; + submit = submitdata; + + input.addEventListener("input", onInputClick); + input.addEventListener("keydown", onKeyDown); + window.addEventListener('resize', debounce(setListLocation, 150)); + document.addEventListener("click", resetResults); + } + } +}()); diff --git a/web/html/login.php b/web/html/login.php index 01454414..3f3d66cc 100644 --- a/web/html/login.php +++ b/web/html/login.php @@ -9,6 +9,10 @@ if (!$disable_http_login || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'])) { $login_error = $login['error']; } +$referer = in_request('referer'); +if ($referer === '') + $referer = $_SERVER['HTTP_REFERER']; + html_header('AUR ' . __("Login")); ?>
    @@ -40,10 +44,15 @@ html_header('AUR ' . __("Login"));

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

    diff --git a/web/html/logout.php b/web/html/logout.php index 14022001..9fd63943 100644 --- a/web/html/logout.php +++ b/web/html/logout.php @@ -5,16 +5,28 @@ set_include_path(get_include_path() . PATH_SEPARATOR . '../lib'); include_once("aur.inc.php"); # access AUR common functions include_once("acctfuncs.inc.php"); # access AUR common functions +$redirect_uri = '/'; + # if they've got a cookie, log them out - need to do this before # sending any HTML output. # if (isset($_COOKIE["AURSID"])) { + $uid = uid_from_sid($_COOKIE['AURSID']); delete_session_id($_COOKIE["AURSID"]); # setting expiration to 1 means '1 second after midnight January 1, 1970' setcookie("AURSID", "", 1, "/", null, !empty($_SERVER['HTTPS']), true); unset($_COOKIE['AURSID']); clear_expired_sessions(); + + # If the account is linked to an SSO account, disconnect the user from the SSO too. + if (isset($uid)) { + $dbh = DB::connect(); + $sso_account_id = $dbh->query("SELECT SSOAccountID FROM Users WHERE ID = " . $dbh->quote($uid)) + ->fetchColumn(); + if ($sso_account_id) + $redirect_uri = '/sso/logout'; + } } -header('Location: /'); +header("Location: $redirect_uri"); diff --git a/web/html/modified-rss.php b/web/html/modified-rss.php new file mode 100644 index 00000000..4c5c47e0 --- /dev/null +++ b/web/html/modified-rss.php @@ -0,0 +1,62 @@ +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 index db9606d9..d19d2ad1 100644 --- a/web/html/packages.php +++ b/web/html/packages.php @@ -46,70 +46,99 @@ if (isset($pkgname)) { html_header($title, $details); ?> - diff --git a/web/html/pkgmerge.php b/web/html/pkgmerge.php index 6ee7423d..d96562a7 100644 --- a/web/html/pkgmerge.php +++ b/web/html/pkgmerge.php @@ -25,7 +25,7 @@ if (has_credential(CRED_PKGBASE_DELETE)): ?>

    -
    +
    @@ -33,25 +33,17 @@ if (has_credential(CRED_PKGBASE_DELETE)): ?> - - +

    -

    +

    " />

    diff --git a/web/lib/DB.class.php b/web/lib/DB.class.php index dfdbbf96..c7b3c745 100644 --- a/web/lib/DB.class.php +++ b/web/lib/DB.class.php @@ -20,15 +20,23 @@ class DB { $backend = config_get('database', 'backend'); $host = config_get('database', 'host'); $socket = config_get('database', 'socket'); + $port = config_get('database', 'port'); $name = config_get('database', 'name'); $user = config_get('database', 'user'); $password = config_get('database', 'password'); if ($backend == "mysql") { - $dsn = $backend . - ':host=' . $host . - ';unix_socket=' . $socket . - ';dbname=' . $name; + if ($port != '') { + $dsn = $backend . + ':host=' . $host . + ';port=' . $port . + ';dbname=' . $name; + } else { + $dsn = $backend . + ':host=' . $host . + ';unix_socket=' . $socket . + ';dbname=' . $name; + } self::$dbh = new PDO($dsn, $user, $password); self::$dbh->exec("SET NAMES 'utf8' COLLATE 'utf8_general_ci';"); diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php index e1c93d27..0d021f99 100644 --- a/web/lib/acctfuncs.inc.php +++ b/web/lib/acctfuncs.inc.php @@ -1329,7 +1329,6 @@ function notify($params) { $descspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), - 2 => array('pipe', 'w') ); $p = proc_open($cmd, $descspec, $pipes); @@ -1340,7 +1339,6 @@ function notify($params) { fclose($pipes[0]); fclose($pipes[1]); - fclose($pipes[2]); return proc_close($p); } diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php index 9c8abe39..e7bc7f97 100644 --- a/web/lib/aurjson.class.php +++ b/web/lib/aurjson.class.php @@ -1,6 +1,7 @@ version = intval($http_data['v']); } - if ($this->version < 1 || $this->version > 5) { + if ($this->version < 1 || $this->version > 6) { return $this->json_error('Invalid version specified.'); } @@ -140,7 +141,7 @@ class AurJSON { } /* - * Check if an IP needs to be rate limited. + * Check if an IP needs to be rate limited. * * @param $ip IP of the current request * @@ -192,7 +193,7 @@ class AurJSON { $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)) { + set_cache_value('ratelimit:' . $ip, 1, $window_length)) { return; } } else { @@ -379,7 +380,7 @@ class AurJSON { } elseif ($this->version >= 2) { if ($this->version == 2 || $this->version == 3) { $fields = implode(',', self::$fields_v2); - } else if ($this->version == 4 || $this->version == 5) { + } else if ($this->version >= 4 && $this->version <= 6) { $fields = implode(',', self::$fields_v4); } $query = "SELECT {$fields} " . @@ -501,13 +502,21 @@ class AurJSON { if (strlen($keyword_string) < 2) { return $this->json_error('Query arg too small.'); } - $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 OR "; - $where_condition .= "Description LIKE $keyword_string)"; + 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)) { diff --git a/web/lib/confparser.inc.php b/web/lib/confparser.inc.php index 1152e132..fdd2b78e 100644 --- a/web/lib/confparser.inc.php +++ b/web/lib/confparser.inc.php @@ -30,7 +30,9 @@ function config_get($section, $key) { global $AUR_CONFIG; config_load(); - return $AUR_CONFIG[$section][$key]; + return isset($AUR_CONFIG[$section][$key]) + ? $AUR_CONFIG[$section][$key] + : null; } function config_get_int($section, $key) { diff --git a/web/lib/feedcreator.class.php b/web/lib/feedcreator.class.php index 802eebbe..bfc29b20 100644 --- a/web/lib/feedcreator.class.php +++ b/web/lib/feedcreator.class.php @@ -1,1541 +1,1546 @@ -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, $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."\">\n"; - $feed.= " \n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."\n"; - $this->descriptionTruncSize = 500; - $feed.= " ".$this->getDescription()."\n"; - $feed.= " ".$this->link."\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.= " ".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"); - - - -***************************************************************************/ - -?> +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/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php index 4c8abba7..a053962e 100644 --- a/web/lib/pkgbasefuncs.inc.php +++ b/web/lib/pkgbasefuncs.inc.php @@ -96,7 +96,6 @@ function render_comment($id) { $descspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), - 2 => array('pipe', 'w') ); $p = proc_open($cmd, $descspec, $pipes); @@ -107,7 +106,6 @@ function render_comment($id) { fclose($pipes[0]); fclose($pipes[1]); - fclose($pipes[2]); return proc_close($p); } @@ -1191,7 +1189,8 @@ function pkgbase_get_comaintainer_uids($base_ids) { * @return array Tuple of success/failure indicator and error message */ function pkgbase_set_comaintainers($base_id, $users, $override=false) { - if (!$override && !has_credential(CRED_PKGBASE_EDIT_COMAINTAINERS, array(pkgbase_maintainer_uid($base_id)))) { + $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.")); } @@ -1209,9 +1208,12 @@ function pkgbase_set_comaintainers($base_id, $users, $override=false) { 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; } - - $uids_new[] = $uid; } $q = sprintf("SELECT UsersID FROM PackageComaintainers WHERE PackageBaseID = %d", $base_id); diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php index 80758005..140c7ec1 100644 --- a/web/lib/pkgfuncs.inc.php +++ b/web/lib/pkgfuncs.inc.php @@ -696,8 +696,8 @@ function pkg_search_page($params, $show_headers=true, $SID="") { $q_where .= "AND (PackageBases.Name LIKE " . $dbh->quote($K) . ") "; } elseif (isset($params["SeB"]) && $params["SeB"] == "k") { - /* Search by keywords. */ - $q_where .= construct_keyword_search($dbh, $params['K'], false); + /* 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). */ @@ -709,7 +709,7 @@ function pkg_search_page($params, $show_headers=true, $SID="") { } else { /* Keyword search (default). */ - $q_where .= construct_keyword_search($dbh, $params['K'], true); + $q_where .= construct_keyword_search($dbh, $params['K'], true, true); } } @@ -832,10 +832,11 @@ function pkg_search_page($params, $show_headers=true, $SID="") { * @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) { +function construct_keyword_search($dbh, $keywords, $namedesc, $keyword=false) { $count = 0; $where_part = ""; $q_keywords = ""; @@ -860,13 +861,18 @@ function construct_keyword_search($dbh, $keywords, $namedesc) { $term = "%" . addcslashes($term, '%_') . "%"; $q_keywords .= $op . " ("; + $q_keywords .= "Packages.Name LIKE " . $dbh->quote($term) . " "; if ($namedesc) { - $q_keywords .= "Packages.Name LIKE " . $dbh->quote($term) . " OR "; - $q_keywords .= "Description LIKE " . $dbh->quote($term) . " OR "; + $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 .= ") "; } - $q_keywords .= "EXISTS (SELECT * FROM PackageKeywords WHERE "; - $q_keywords .= "PackageKeywords.PackageBaseID = Packages.PackageBaseID AND "; - $q_keywords .= "PackageKeywords.Keyword LIKE " . $dbh->quote($term) . ")) "; $count++; if ($count >= 20) { @@ -919,13 +925,13 @@ function sanitize_ids($ids) { * * @return array $packages Package info for the specified number of recent packages */ -function latest_pkgs($numpkgs) { +function latest_pkgs($numpkgs, $orderBy='SubmittedTS') { $dbh = DB::connect(); - $q = "SELECT Packages.*, MaintainerUID, SubmittedTS "; + $q = "SELECT Packages.*, MaintainerUID, SubmittedTS, ModifiedTS "; $q.= "FROM Packages LEFT JOIN PackageBases ON "; $q.= "PackageBases.ID = Packages.PackageBaseID "; - $q.= "ORDER BY SubmittedTS DESC "; + $q.= "ORDER BY " . $orderBy . " DESC "; $q.= "LIMIT " . intval($numpkgs); $result = $dbh->query($q); @@ -938,3 +944,14 @@ function latest_pkgs($numpkgs) { 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/routing.inc.php b/web/lib/routing.inc.php index 7d9750a0..73c667d2 100644 --- a/web/lib/routing.inc.php +++ b/web/lib/routing.inc.php @@ -15,6 +15,7 @@ $ROUTES = array( '/logout' => 'logout.php', '/passreset' => 'passreset.php', '/rpc' => 'rpc.php', + '/rss/modified' => 'modified-rss.php', '/rss' => 'rss.php', '/tos' => 'tos.php', '/tu' => 'tu.php', diff --git a/web/template/footer.php b/web/template/footer.php index 572dbb26..7f97aae0 100644 --- a/web/template/footer.php +++ b/web/template/footer.php @@ -3,7 +3,7 @@ diff --git a/web/template/pkg_details.php b/web/template/pkg_details.php index c6bb32d8..047de9a7 100644 --- a/web/template/pkg_details.php +++ b/web/template/pkg_details.php @@ -308,14 +308,10 @@ endif;
    diff --git a/web/template/pkg_search_results.php b/web/template/pkg_search_results.php index 7c5ad03b..61335560 100644 --- a/web/template/pkg_search_results.php +++ b/web/template/pkg_search_results.php @@ -96,7 +96,7 @@ if (!$result): ?> - + diff --git a/web/template/pkgbase_actions.php b/web/template/pkgbase_actions.php index 5eee5478..3d208328 100644 --- a/web/template/pkgbase_actions.php +++ b/web/template/pkgbase_actions.php @@ -7,7 +7,7 @@
  • -
  • +
  • diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php index a6857c4e..35ad217a 100644 --- a/web/template/pkgbase_details.php +++ b/web/template/pkgbase_details.php @@ -137,14 +137,10 @@ endif; diff --git a/web/template/pkgreq_form.php b/web/template/pkgreq_form.php index 904ab48f..9d74093e 100644 --- a/web/template/pkgreq_form.php +++ b/web/template/pkgreq_form.php @@ -9,7 +9,7 @@
  • - +
    @@ -24,44 +24,41 @@

    - - +

    - +

    diff --git a/web/template/stats/updates_table.php b/web/template/stats/updates_table.php index b4c6215f..23a86288 100644 --- a/web/template/stats/updates_table.php +++ b/web/template/stats/updates_table.php @@ -1,6 +1,7 @@

    ()

    -RSS Feed +RSS Feed +RSS Feed