├── .coveragerc ├── .flake8 ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .gitlab-ci.yml ├── .pyup.yml ├── AUTHORS ├── HACKING ├── LICENSE ├── Makefile ├── README ├── README.md ├── archweb.wsgi ├── conftest.py ├── devel ├── __init__.py ├── admin.py ├── fields.py ├── fixtures │ ├── core.db.tar.gz │ └── staff_groups.json ├── forms.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── archweb_inotify.py │ │ ├── generate_keyring.py │ │ ├── pgp_import.py │ │ ├── read_rebuilderd_status.py │ │ ├── readlinks.py │ │ ├── readlinks_inotify.py │ │ ├── rematch_developers.py │ │ ├── reporead.py │ │ ├── reporead_inotify.py │ │ └── retire_user.py ├── migrations │ ├── 0001_squashed_0002_staffgroup.py │ ├── 0002_auto_20181216_1605.py │ ├── 0003_auto_20191009_1924.py │ ├── 0004_userprofile_website_rss.py │ ├── 0005_auto_20200628_1600.py │ ├── 0006_userprofile_rebuilderd_updates.py │ ├── 0007_auto_20210523_2038.py │ ├── 0008_alter_userprofile_repos_auth_token_and_more.py │ ├── 0009_alter_userprofile_public_email_and_more.py │ ├── 0009_alter_userprofile_time_zone_alter_userprofile_yob.py │ ├── 0010_merge_20230312_1527.py │ ├── 0011_userprofile_social.py │ └── __init__.py ├── models.py ├── reports.py ├── templatetags │ ├── __init__.py │ └── group.py ├── tests │ ├── __init__.py │ ├── test_devel.py │ ├── test_pgp_import.py │ ├── test_rematch_developers.py │ ├── test_reporead.py │ ├── test_reports.py │ ├── test_retire_user.py │ ├── test_templatetags.py │ └── test_user.py ├── urls.py ├── utils.py └── views.py ├── docs └── mirror_access.md ├── feeds.py ├── local_settings.py.example ├── main ├── __init__.py ├── admin.py ├── context_processors.py ├── fixtures │ ├── arches.json │ ├── denylist.json │ ├── groups.json │ ├── package.json │ └── repos.json ├── log.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── donor_import.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_repo_public_testing.py │ ├── 0003_rebuilderdstatus.py │ ├── 0004_rebuilderdstatus_build_id.py │ ├── 0004_soname.py │ ├── 0005_merge_0004_rebuilderdstatus_build_id_0004_soname.py │ └── __init__.py ├── models.py ├── storage.py ├── templatetags │ ├── __init__.py │ ├── attributes.py │ ├── cdn.py │ ├── details_link.py │ ├── flags.py │ └── pgp.py ├── tests │ ├── __init__.py │ ├── test_donor_import.py │ ├── test_templatetags_flags.py │ └── test_templatetags_pgp.py └── utils.py ├── manage.py ├── mirrors ├── __init__.py ├── admin.py ├── fields.py ├── fixtures │ └── mirrorprotocols.json ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── mirrorcheck.py │ │ └── mirrorresolv.py ├── migrations │ ├── 0001_squashed_0002_mirrorurl_bandwidth.py │ └── __init__.py ├── models.py ├── static │ └── mirror_status.js ├── templatetags │ ├── __init__.py │ └── mirror_status.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_mirrorcheck.py │ ├── test_mirrorlist.py │ ├── test_mirrorlocations.py │ ├── test_mirrorresolv.py │ ├── test_mirrorrsync.py │ ├── test_mirrors.py │ ├── test_mirrorstatus.py │ ├── test_mirrorurl.py │ ├── test_models.py │ └── test_templatetags.py ├── urls.py ├── urls_mirrorlist.py ├── utils.py └── views │ ├── __init__.py │ ├── api.py │ └── mirrorlist.py ├── news ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_squashed_0002_news_send_announce.py │ └── __init__.py ├── models.py ├── tests │ ├── test_crud.py │ └── test_models.py ├── urls.py └── views.py ├── packages ├── __init__.py ├── admin.py ├── alpm.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── populate_signoffs.py ├── migrations │ ├── 0001_squashed_0003_auto_20170524_0704.py │ ├── 0002_flagdenylist.py │ └── __init__.py ├── models.py ├── sql │ ├── search_indexes.postgresql_psycopg2.sql │ ├── update.postgresql_psycopg2.sql │ └── update.sqlite3.sql ├── templatetags │ ├── __init__.py │ └── package_extras.py ├── tests │ ├── test_adopt.py │ ├── test_alpm.py │ ├── test_flag_packages.py │ ├── test_populate_signoffs.py │ ├── test_search.py │ ├── test_signoffs.py │ └── test_views.py ├── urls.py ├── urls_groups.py ├── utils.py └── views │ ├── __init__.py │ ├── display.py │ ├── flag.py │ ├── search.py │ └── signoff.py ├── planet ├── __init__.py ├── admin.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── update_planet.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_feeditem_feed.py │ └── __init__.py ├── models.py ├── tests │ ├── __init__.py │ ├── test_command.py │ └── test_views.py └── views.py ├── public ├── __init__.py ├── management │ └── __init__.py ├── static │ └── logos │ │ ├── archlinux-logo-black-1200dpi.png │ │ ├── archlinux-logo-black-90dpi.png │ │ ├── archlinux-logo-black-scalable.svg │ │ ├── archlinux-logo-dark-1200dpi.png │ │ ├── archlinux-logo-dark-90dpi.png │ │ ├── archlinux-logo-dark-scalable.svg │ │ ├── archlinux-logo-light-1200dpi.png │ │ ├── archlinux-logo-light-90dpi.png │ │ ├── archlinux-logo-light-scalable.svg │ │ ├── archlinux-logo-only.svg │ │ ├── archlinux-logo-white-1200dpi.png │ │ ├── archlinux-logo-white-90dpi.png │ │ ├── archlinux-logo-white-scalable.svg │ │ └── legacy │ │ ├── arch-legacy-aqua-blue.png │ │ ├── arch-legacy-aqua-blue.svg │ │ ├── arch-legacy-aqua-white.png │ │ ├── arch-legacy-aqua-white.svg │ │ ├── arch-legacy-aqua.png │ │ ├── arch-legacy-aqua.svg │ │ ├── arch-legacy-blue1.png │ │ ├── arch-legacy-blue1.svg │ │ ├── arch-legacy-blue2.png │ │ ├── arch-legacy-blue2.svg │ │ ├── arch-legacy-noodle-blue.png │ │ ├── arch-legacy-noodle-blue.svg │ │ ├── arch-legacy-noodle-box.png │ │ ├── arch-legacy-noodle-box.svg │ │ ├── arch-legacy-noodle-cup.png │ │ ├── arch-legacy-noodle-cup.svg │ │ ├── arch-legacy-noodle-white.png │ │ ├── arch-legacy-noodle-white.svg │ │ ├── arch-legacy-ribbon1.png │ │ ├── arch-legacy-ribbon2.png │ │ ├── arch-legacy-ribbon3.png │ │ ├── arch-legacy-ribbon4.png │ │ ├── arch-legacy-ribbon5.png │ │ ├── arch-legacy-ribbon6.png │ │ ├── arch-legacy-wombat-lg.png │ │ └── arch-legacy-wombat.png ├── tests.py ├── utils.py └── views.py ├── releng ├── __init__.py ├── admin.py ├── fixtures │ └── release.json ├── migrations │ ├── 0001_squashed_0005_auto_20180616_0947.py │ ├── 0002_auto_20181216_1605.py │ ├── 0003_release_pgp_key.py │ ├── 0004_release_wkd_email.py │ ├── 0005_release_b2_sum_release_sha256_sum.py │ ├── 0006_alter_release_b2_sum.py │ └── __init__.py ├── models.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_models.py │ └── test_views.py ├── urls.py └── views.py ├── requirements.txt ├── requirements_prod.txt ├── requirements_test.txt ├── ruff.toml ├── settings.py ├── sitemaps.py ├── sitestatic ├── archnavbar │ ├── archlogo.png │ ├── archlogo.svg │ └── archnavbar.css ├── archweb.css ├── archweb.js ├── click_and_pledge.png ├── download.png ├── favicon.png ├── flaghelp.css ├── flags │ ├── fam.css │ └── fam.png ├── hetzner_logo.png ├── homepage.js ├── icons8_logo.png ├── jquery-3.6.0.min.js ├── jquery.tablesorter-2.31.0.js ├── jquery.tablesorter-2.31.0.min.js ├── logos │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-72x72.png │ └── icon-transparent-64x64.png ├── magnet.png ├── netboot │ ├── ipxe-arch.efi │ ├── ipxe-arch.efi.sig │ ├── ipxe-arch.lkrn │ ├── ipxe-arch.lkrn.sig │ ├── ipxe-arch.pxe │ └── ipxe-arch.pxe.sig ├── nitrokey_logo.png ├── rss.svg ├── shells_logo.png └── silhouette.png ├── templates ├── 403.html ├── 404.html ├── 500.html ├── admin │ └── index.html ├── base.html ├── devel │ ├── admin_log.html │ ├── clock.html │ ├── email_reproduciblebuilds.txt │ ├── index.html │ ├── new_account.txt │ ├── packages.html │ ├── profile.html │ ├── stats.html │ └── tier0_mirror.html ├── general_form.html ├── mirrors │ ├── error_table.html │ ├── mirror_details.html │ ├── mirror_details_urls.html │ ├── mirrorlist.txt │ ├── mirrorlist_generate.html │ ├── mirrorlist_status.txt │ ├── mirrors.html │ ├── status.html │ ├── status_table.html │ ├── url_details.html │ └── url_details_logs.html ├── news │ ├── add.html │ ├── delete.html │ ├── list.html │ ├── news_email_notification.txt │ ├── paginator.html │ └── view.html ├── packages │ ├── approved.txt │ ├── details.html │ ├── details_depend.html │ ├── details_relatedto.html │ ├── details_requiredby.html │ ├── differences.html │ ├── files.html │ ├── files_list.html │ ├── flag.html │ ├── flag_confirmed.html │ ├── flagged.html │ ├── flaghelp.html │ ├── groups.html │ ├── opensearch.xml │ ├── outofdate.txt │ ├── package_details.html │ ├── packages_list.html │ ├── removed.html │ ├── search.html │ ├── search_paginator.html │ ├── signoff_cell.html │ ├── signoff_options.html │ ├── signoffs.html │ ├── sonames.html │ ├── sonames_list.html │ └── stale_relations.html ├── planet │ └── index.html ├── public │ ├── about.html │ ├── art.html │ ├── blank.html │ ├── developer_list.html │ ├── donate.html │ ├── download.html │ ├── feeds.html │ ├── index.html │ ├── keys.html │ └── userlist.html ├── registration │ ├── login.html │ └── logout.html ├── releng │ ├── archlinux.ipxe │ ├── netboot.html │ ├── release_detail.html │ └── release_list.html ├── sitemaps │ ├── news_sitemap.xml │ └── sitemap.xml ├── todolists │ ├── complete_email_notification.txt │ ├── email_notification.txt │ ├── list.html │ ├── paginator.html │ ├── todolist_confirm_delete.html │ └── view.html └── visualize │ └── index.html ├── todolists ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_squashed_0002_remove_todolist_old_id.py │ ├── 0002_todolist_kind.py │ └── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── todolists.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_models.py │ ├── test_templatetags_todolists.py │ └── test_views.py ├── urls.py ├── utils.py └── views.py ├── urls.py └── visualize ├── __init__.py ├── static ├── d3-3.5.0.js ├── d3-3.5.0.min.js └── visualize.js ├── tests └── test_urls.py ├── urls.py └── views.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | 3 | [run] 4 | omit = 5 | **/migrations/*.py 6 | **/tests.py 7 | **/tests/*.py 8 | env* 9 | settings.py 10 | local_settings.py 11 | manage.py 12 | conftest.py 13 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 118 3 | ignore = E731, E241, E741 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.html diff=html 2 | *.py diff=python 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://archlinux.org/donate/'] 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Github-Actions 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Python 3.13 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.13 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install -r requirements.txt && pip install -r requirements_test.txt && pip install ruff 20 | - name: Run ruff 21 | run: | 22 | ruff check . 23 | - name: Lint with flake8 24 | run: | 25 | make lint 26 | - name: Run collectstatic 27 | run: | 28 | make collectstatic 29 | - name: Run tests 30 | run: | 31 | make coverage 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.swo 4 | .DS_Store 5 | local_settings.py 6 | archweb.db 7 | archweb.db-* 8 | database.db 9 | tags 10 | collected_static/ 11 | testing/ 12 | env/ 13 | htmlcov 14 | **/**.tar.gz 15 | /.idea* 16 | **/**__pycache__ 17 | 18 | .mypy_cache/ 19 | 20 | # rope 21 | .ropeproject/ 22 | 23 | # coverage 24 | .coverage 25 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: "archlinux:latest" 2 | 3 | before_script: 4 | - pacman -Syu --needed --noconfirm python-pip git 5 | - pip install coverage flake8 6 | - pip install -r requirements.txt 7 | - python manage.py collectstatic --noinput 8 | 9 | lint: 10 | script: 11 | - flake8 --exclude "*/migrations/" devel main mirrors news packages releng templates todolists visualize *.py 12 | 13 | # TODO: https://docs.gitlab.com/ee/ci/junit_test_reports.html 14 | test: 15 | script: 16 | - coverage run --rcfile .coveragerc manage.py test 17 | -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | # autogenerated pyup.io config file 2 | # see https://pyup.io/docs/configuration/ for all available options 3 | 4 | update: insecure 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # CURRENT MAINTAINERS 2 | Jelle van der Waa 3 | Dan McGee 4 | 5 | # OTHER AUTHORS 6 | Aaron Griffin 7 | Alad Wenter 8 | Andrea Scarpino 9 | Angel Velasquez 10 | Christian Hesse 11 | Dan McGee 12 | Daniel Hahler 13 | Dario Giovannetti 14 | Dieter Plaetinck 15 | Dusty Phillips 16 | Eliott 17 | Eli Schwartz 18 | Evangelos Foutras 19 | Felix Yan 20 | Florian Pritz 21 | Frank Vanderham 22 | Genki Sky 23 | Giancarlo Razzolini 24 | Ismael Carnales 25 | Jakub Klinkovský 26 | Jelle van der Waa 27 | Johannes Löthberg 28 | Judd Vinet 29 | Levente Polyak 30 | Lukas Fleischer 31 | Olivier Keun 32 | Pierre Schmitz 33 | PyroPeter 34 | Sergej Pupykin 35 | Simo Leone 36 | Thayer Williams 37 | Thomas Bächler 38 | Tom Willemsen 39 | Tyler Dence 40 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====================== 3 | 4 | Coding Style 5 | ------------ 6 | 7 | 1. All code should be indented with spaces. This is effectively the following VIM modeline: 8 | /* vim: set ai ts=4 sw=4 et: */ 9 | 10 | 2. Recommend removing trailing whitespace. Here is an example for .vimrc 11 | autocmd BufWritePre *.py normal m`:%s/\s\+$//e `` 12 | 13 | 3. Wrap lines at 80 characters MAXIMUM 14 | 15 | 16 | vim: set ai ts=4 sw=4 et: 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON>=python 2 | PYTEST?=pytest 3 | PYTEST_OPTIONS+= 4 | PYTEST_INPUT?=. 5 | PYTEST_COVERAGE_OPTIONS+=--cov-report=term-missing --cov-report=html:htmlcov --cov=. 6 | PYTEST_PDB?=0 7 | PYTEST_PDB_OPTIONS?=--pdb --pdbcls=IPython.terminal.debugger:TerminalPdb 8 | 9 | 10 | ifeq (${PYTEST_PDB},1) 11 | PYTEST_OPTIONS+= ${PYTEST_PDB_OPTIONS} 12 | else 13 | test-pdb: PYTEST_OPTIONS+= ${PYTEST_PDB_OPTIONS} 14 | endif 15 | test-pdb: test 16 | 17 | 18 | .PHONY: test lint coverage 19 | 20 | lint: 21 | flake8 --extend-exclude "*/migrations/,local_settings.py" devel main mirrors news packages releng templates todolists visualize *.py 22 | 23 | collectstatic: 24 | python manage.py collectstatic --noinput 25 | 26 | test: test-py 27 | 28 | test-py coverage: 29 | DJANGO_SETTINGS_MODULE=settings ${PYTEST} ${PYTEST_OPTIONS} ${PYTEST_COVERAGE_OPTIONS} ${PYTEST_INPUT} 30 | 31 | open-coverage: coverage 32 | ${BROWSER} htmlcov/index.html 33 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /archweb.wsgi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import os 3 | import sys 4 | import site 5 | 6 | base_path = "/srv/http/archweb" 7 | py_version = sys.version_info 8 | 9 | site.addsitedir('/srv/http/archweb-env/lib/python{}.{}/site-packages'.format(py_version.major, py_version.minor)) 10 | sys.path.insert(0, base_path) 11 | 12 | os.environ['DJANGO_SETTINGS_MODULE'] = "settings" 13 | 14 | os.chdir(base_path) 15 | 16 | from django.core.wsgi import get_wsgi_application 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.contrib.auth.models import Group 3 | from django.core.management import call_command 4 | 5 | from devel.models import UserProfile 6 | from main.models import Repo 7 | 8 | USERNAME = 'joeuser' 9 | FIRSTNAME = 'Joe' 10 | LASTNAME = 'User' 11 | EMAIL = 'user1@example.com' 12 | 13 | 14 | @pytest.fixture 15 | def arches(db): 16 | # TODO: create Arch object 17 | call_command('loaddata', 'main/fixtures/arches.json') 18 | 19 | 20 | @pytest.fixture 21 | def repos(arches): 22 | # TODO: create Repo object 23 | call_command('loaddata', 'main/fixtures/repos.json') 24 | 25 | 26 | @pytest.fixture 27 | def package(db, arches, repos): 28 | # TODO: convert to create_package with standard parameters 29 | call_command('loaddata', 'main/fixtures/package.json') 30 | 31 | 32 | @pytest.fixture 33 | def groups(db): 34 | call_command('loaddata', 'main/fixtures/groups.json') 35 | 36 | 37 | @pytest.fixture 38 | def staff_groups(db): 39 | call_command('loaddata', 'devel/fixtures/staff_groups.json') 40 | 41 | 42 | @pytest.fixture 43 | def user(django_user_model): 44 | user = django_user_model.objects.create_user(username=USERNAME, password=USERNAME, email=EMAIL) 45 | yield user 46 | user.delete() 47 | 48 | 49 | @pytest.fixture 50 | def userprofile(user): 51 | profile = UserProfile.objects.create(user=user, 52 | public_email=f'{user.username}@archlinux.org') 53 | yield profile 54 | profile.delete() 55 | 56 | 57 | @pytest.fixture 58 | def developer(user, userprofile, repos, groups): 59 | user.groups.add(Group.objects.get(name='Developers')) 60 | userprofile.allowed_repos.add(Repo.objects.get(name='Core')) 61 | return user 62 | 63 | 64 | @pytest.fixture 65 | def developer_client(client, developer, userprofile, groups): 66 | client.login(username=USERNAME, password=USERNAME) 67 | return client 68 | 69 | 70 | @pytest.fixture 71 | def denylist(db): 72 | # TODO: create Denylist object 73 | call_command('loaddata', 'main/fixtures/denylist.json') 74 | 75 | 76 | @pytest.fixture(autouse=True) 77 | def use_dummy_cache_backend(settings): 78 | settings.CACHES = { 79 | "default": { 80 | "BACKEND": "django.core.cache.backends.dummy.DummyCache", 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /devel/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/devel/__init__.py -------------------------------------------------------------------------------- /devel/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | from django.contrib.auth.models import User 4 | 5 | from .models import DeveloperKey, MasterKey, PGPSignature, StaffGroup, UserProfile 6 | 7 | 8 | class UserProfileInline(admin.StackedInline): 9 | model = UserProfile 10 | 11 | 12 | class UserProfileAdmin(UserAdmin): 13 | inlines = [UserProfileInline] 14 | list_display = ('username', 'email', 'first_name', 'last_name', 'last_login', 'is_staff', 'is_active') 15 | list_filter = ('is_staff', 'is_superuser', 'is_active', 'groups') 16 | 17 | 18 | class StaffGroupAdmin(admin.ModelAdmin): 19 | list_display = ('name', 'group', 'sort_order', 'member_title', 'slug') 20 | prepopulated_fields = {'slug': ('name',)} 21 | 22 | 23 | class MasterKeyAdmin(admin.ModelAdmin): 24 | list_display = ('pgp_key', 'owner', 'created', 'revoker', 'revoked') 25 | search_fields = ('pgp_key', 'owner__username', 'revoker__username') 26 | date_hierarchy = 'created' 27 | 28 | 29 | class DeveloperKeyAdmin(admin.ModelAdmin): 30 | list_display = ('key', 'parent', 'owner', 'created', 'expires', 'revoked') 31 | search_fields = ('key', 'owner__username') 32 | list_filter = ('owner',) 33 | date_hierarchy = 'created' 34 | 35 | 36 | class PGPSignatureAdmin(admin.ModelAdmin): 37 | list_display = ('signer', 'signee', 'created', 'expires', 'revoked') 38 | search_fields = ('signer', 'signee') 39 | date_hierarchy = 'created' 40 | 41 | 42 | admin.site.unregister(User) 43 | admin.site.register(User, UserProfileAdmin) 44 | admin.site.register(StaffGroup, StaffGroupAdmin) 45 | 46 | admin.site.register(MasterKey, MasterKeyAdmin) 47 | admin.site.register(DeveloperKey, DeveloperKeyAdmin) 48 | admin.site.register(PGPSignature, PGPSignatureAdmin) 49 | 50 | # vim: set ts=4 sw=4 et: 51 | -------------------------------------------------------------------------------- /devel/fields.py: -------------------------------------------------------------------------------- 1 | from django.core.validators import RegexValidator 2 | from django.db import models 3 | 4 | 5 | class PGPKeyField(models.CharField): 6 | def __init__(self, *args, **kwargs): 7 | super(PGPKeyField, self).__init__(*args, **kwargs) 8 | self.validators.append( 9 | RegexValidator(r'^[0-9A-F]{40}$', "Ensure this value consists of 40 hex characters.", 'hex_char')) 10 | 11 | def to_python(self, value): 12 | if value == '' or value is None: 13 | return None 14 | value = super(PGPKeyField, self).to_python(value) 15 | # remove all spaces 16 | value = value.replace(' ', '') 17 | # prune prefixes, either 0x or 2048R/ type 18 | if value.startswith('0x'): 19 | value = value[2:] 20 | value = value.split('/')[-1] 21 | # make all (hex letters) uppercase 22 | return value.upper() 23 | 24 | def formfield(self, **kwargs): 25 | # override so we don't set max_length form field attribute 26 | return models.Field.formfield(self, **kwargs) 27 | 28 | # vim: set ts=4 sw=4 et: 29 | -------------------------------------------------------------------------------- /devel/fixtures/core.db.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/devel/fixtures/core.db.tar.gz -------------------------------------------------------------------------------- /devel/fixtures/staff_groups.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "group": [ 5 | "Developers" 6 | ], 7 | "description": "This is a list of the current Arch Linux Developers. They maintain the [core] and [extra] package repositories in addition to doing any other developer duties.", 8 | "sort_order": 1, 9 | "member_title": "Developer", 10 | "slug": "developers", 11 | "name": "Developers" 12 | }, 13 | "model": "devel.staffgroup", 14 | "pk": 1 15 | }, 16 | { 17 | "fields": { 18 | "group": [ 19 | "Package Maintainers" 20 | ], 21 | "description": "Here are all your friendly Arch Linux Package Maintainers who are in charge of the Archlinux's official repositories (aside from [core] / [core-testing]).", 22 | "sort_order": 2, 23 | "member_title": "Package Maintainer", 24 | "slug": "package-maintainers", 25 | "name": "Package Maintainers" 26 | }, 27 | "model": "devel.staffgroup", 28 | "pk": 2 29 | }, 30 | { 31 | "fields": { 32 | "group": [ 33 | "Retired Developers" 34 | ], 35 | "description": "Below you can find a list of ex-developers (aka project fellows). These folks helped make Arch what it is today. Thanks!", 36 | "sort_order": 11, 37 | "member_title": "Fellow", 38 | "slug": "developer-fellows", 39 | "name": "Developer Fellows" 40 | }, 41 | "model": "devel.staffgroup", 42 | "pk": 3 43 | }, 44 | { 45 | "fields": { 46 | "group": [ 47 | "Retired Package Maintainers" 48 | ], 49 | "description": "Below you can find a list of ex-package maintainers (aka fellows). These folks helped make Arch what it is today. Thanks!", 50 | "sort_order": 12, 51 | "member_title": "Fellow", 52 | "slug": "package-maintainer-fellows", 53 | "name": "Package Maintainer Fellows" 54 | }, 55 | "model": "devel.staffgroup", 56 | "pk": 4 57 | }, 58 | { 59 | "fields": { 60 | "group": [ 61 | "Support Staff" 62 | ], 63 | "description": "These are the unheralded people that keep things running behind the scenes. Forum moderators, wiki admins, IRC moderators, mirror maintenance, and everything else that keeps a Linux distro running smoothly.", 64 | "sort_order": 5, 65 | "member_title": "Staff", 66 | "slug": "support-staff", 67 | "name": "Support Staff" 68 | }, 69 | "model": "devel.staffgroup", 70 | "pk": 5 71 | } 72 | ] 73 | -------------------------------------------------------------------------------- /devel/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/devel/management/__init__.py -------------------------------------------------------------------------------- /devel/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/devel/management/commands/__init__.py -------------------------------------------------------------------------------- /devel/management/commands/retire_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | retire_user 4 | 5 | Retire a user by de-activing the account and moving the user to the retired 6 | group. 7 | 8 | Usage ./manage.py retire_user user 9 | """ 10 | 11 | import logging 12 | 13 | from django.contrib.auth.models import Group, User 14 | from django.core.management.base import BaseCommand, CommandError 15 | 16 | from devel.models import UserProfile 17 | 18 | logger = logging.getLogger("command") 19 | logger.setLevel(logging.WARNING) 20 | 21 | MAPPING = { 22 | 'Developers': 'Retired Developers', 23 | 'Package Maintainers': 'Retired Package Maintainers', 24 | 'Support Staff': 'Retired Support Staff', 25 | } 26 | 27 | 28 | class Command(BaseCommand): 29 | help = "Retires a user by deactivating the user and moving the group membership to retired groups." 30 | missing_args_message = 'missing argument user.' 31 | 32 | def add_arguments(self, parser): 33 | parser.add_argument('user', type=str) 34 | 35 | def handle(self, *args, **options): 36 | v = int(options.get('verbosity', 0)) 37 | if v == 0: 38 | logger.level = logging.ERROR 39 | elif v == 1: 40 | logger.level = logging.INFO 41 | elif v >= 2: 42 | logger.level = logging.DEBUG 43 | 44 | try: 45 | user = User.objects.get(username=options['user']) 46 | except User.DoesNotExist: 47 | raise CommandError(u"Failed to find User '{}'".format(options['user'])) 48 | 49 | try: 50 | profile = UserProfile.objects.get(user=user) 51 | except UserProfile.DoesNotExist: 52 | raise CommandError(u"Failed to find UserProfile") 53 | 54 | # Set user inactive 55 | user.is_active = False 56 | 57 | # Clear allowed repos 58 | profile.allowed_repos.clear() 59 | 60 | # Move Groups to Retired. 61 | del_groups = list(user.groups.filter(name__in=MAPPING.keys())) 62 | add_groups = [Group.objects.get(name=MAPPING.get(group.name)) for group in del_groups] 63 | user.groups.remove(*del_groups) 64 | user.groups.add(*add_groups) 65 | user.save() 66 | -------------------------------------------------------------------------------- /devel/migrations/0004_userprofile_website_rss.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.6 on 2019-11-24 16:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('devel', '0003_auto_20191009_1924'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='userprofile', 15 | name='website_rss', 16 | field=models.CharField(blank=True, max_length=200, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /devel/migrations/0005_auto_20200628_1600.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-28 16:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('devel', '0004_userprofile_website_rss'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='userprofile', 15 | name='website_rss', 16 | field=models.CharField(blank=True, help_text='RSS Feed of your website for planet.archlinux.org', max_length=200, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /devel/migrations/0006_userprofile_rebuilderd_updates.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-28 21:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('devel', '0005_auto_20200628_1600'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='userprofile', 15 | name='rebuilderd_updates', 16 | field=models.BooleanField(default=False, help_text='Receive reproducible build package updates'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /devel/migrations/0009_alter_userprofile_public_email_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1 on 2022-08-18 18:08 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('devel', '0008_alter_userprofile_repos_auth_token_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='userprofile', 15 | name='public_email', 16 | field=models.EmailField(help_text='Required field', max_length=50), 17 | ), 18 | migrations.AlterField( 19 | model_name='userprofile', 20 | name='website', 21 | field=models.URLField(blank=True, null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name='userprofile', 25 | name='website_rss', 26 | field=models.URLField(blank=True, help_text='RSS Feed of your website for planet.archlinux.org', null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /devel/migrations/0010_merge_20230312_1527.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.1.6 on 2023-03-12 15:27 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("devel", "0009_alter_userprofile_public_email_and_more"), 9 | ("devel", "0009_alter_userprofile_time_zone_alter_userprofile_yob"), 10 | ] 11 | 12 | operations = [] 13 | -------------------------------------------------------------------------------- /devel/migrations/0011_userprofile_social.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [ 7 | ('devel', '0010_merge_20230312_1527'), 8 | ] 9 | 10 | operations = [ 11 | migrations.AddField( 12 | model_name='userprofile', 13 | name='social', 14 | field=models.CharField(blank=True, max_length=200, null=True), 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /devel/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/devel/migrations/__init__.py -------------------------------------------------------------------------------- /devel/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/devel/templatetags/__init__.py -------------------------------------------------------------------------------- /devel/templatetags/group.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.filter(name='in_group') 7 | def in_group(user, group_name): 8 | return user.groups.filter(name=group_name).exists() 9 | 10 | 11 | @register.filter(name='in_groups') 12 | def in_groups(user, group_names): 13 | group_names = group_names.split(':') 14 | return user.groups.filter(name__in=group_names).exists() 15 | -------------------------------------------------------------------------------- /devel/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/devel/tests/__init__.py -------------------------------------------------------------------------------- /devel/tests/test_devel.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import Group, User 2 | from django.test import TransactionTestCase 3 | 4 | from devel.models import UserProfile 5 | 6 | 7 | class DevelView(TransactionTestCase): 8 | fixtures = ['main/fixtures/arches.json', 'main/fixtures/repos.json', 9 | 'main/fixtures/package.json'] 10 | 11 | def setUp(self): 12 | password = 'test' 13 | self.user = User.objects.create_superuser('admin', 14 | 'admin@archlinux.org', 15 | password) 16 | for name in ['Developers', 'Retired Developers']: 17 | Group.objects.create(name=name) 18 | self.user.groups.add(Group.objects.get(name='Developers')) 19 | self.user.save() 20 | self.profile = UserProfile.objects.create(user=self.user, 21 | public_email=f"{self.user.username}@awesome.com") 22 | self.client.post('/login/', { 23 | 'username': self.user.username, 24 | 'password': password 25 | }) 26 | 27 | def tearDown(self): 28 | self.profile.delete() 29 | self.user.delete() 30 | Group.objects.all().delete() 31 | 32 | def test_clock(self): 33 | response = self.client.get('/devel/clock/') 34 | self.assertEqual(response.status_code, 200) 35 | 36 | def test_profile(self): 37 | response = self.client.get('/devel/profile/') 38 | self.assertEqual(response.status_code, 200) 39 | # Test changing 40 | 41 | def test_stats(self): 42 | response = self.client.get('/devel/stats/') 43 | self.assertEqual(response.status_code, 200) 44 | -------------------------------------------------------------------------------- /devel/tests/test_pgp_import.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.core.management import call_command 4 | from django.core.management.base import CommandError 5 | from django.test import TransactionTestCase 6 | 7 | CREATED = 1541685162 8 | USER = 'John Doe ' 9 | ID1 = 'D6C055F238843F1C' 10 | ID2 = 'D8AFDDA07A5B6EDFA7D8CCDAD6D055F927843F1C' 11 | ID3 = 'B588C0234ECADD3F0BBBEEBA44F9F02E089294E7' 12 | 13 | SIG_DATA = [ 14 | f'pub:-:4096:1:{ID1}:{CREATED}:::-:::scESCA::::::23::0:', 15 | f'fpr:::::::::{ID2}:', 16 | f'uid:-::::{CREATED}::{ID3}::{USER}::::::::::0:', 17 | f'sig:::1:{ID1}:{CREATED}::::{USER}:13x::{ID2}:::10:' 18 | ] 19 | 20 | 21 | class PGPImportTest(TransactionTestCase): 22 | fixtures = ['main/fixtures/arches.json', 'main/fixtures/repos.json'] 23 | 24 | def test_pgp_import_error(self): 25 | with self.assertRaises(CommandError) as e: 26 | call_command('pgp_import') 27 | self.assertIn('keyring_path', str(e.exception)) 28 | 29 | @patch('devel.management.commands.pgp_import.call_gpg') 30 | def test_pgp_import_garbage_data(self, mock_call_gpg): 31 | mock_call_gpg.return_value = 'barf' 32 | with patch('devel.management.commands.pgp_import.logger') as logger: 33 | call_command('pgp_import', '/tmp') 34 | logger.info.assert_called() 35 | logger.info.assert_any_call('created %d, updated %d signatures', 0, 0) 36 | logger.info.assert_any_call('created %d, updated %d keys', 0, 0) 37 | 38 | @patch('devel.management.commands.pgp_import.call_gpg') 39 | def test_pgp_import(self, mock_call_gpg): 40 | mock_call_gpg.return_value = '\n'.join(SIG_DATA) 41 | with patch('devel.management.commands.pgp_import.logger') as logger: 42 | call_command('pgp_import', '/tmp') 43 | logger.info.assert_called() 44 | logger.info.assert_any_call('created %d, updated %d signatures', 0, 0) 45 | logger.info.assert_any_call('created %d, updated %d keys', 1, 0) 46 | -------------------------------------------------------------------------------- /devel/tests/test_rematch_developers.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import patch 2 | 3 | from django.core.management import call_command 4 | from django.test import TransactionTestCase 5 | 6 | 7 | class RematchDeveloperTest(TransactionTestCase): 8 | fixtures = ['main/fixtures/arches.json', 'main/fixtures/repos.json'] 9 | 10 | def test_rematch_developers(self): 11 | with patch('devel.management.commands.rematch_developers.logger') as logger: 12 | call_command('rematch_developers') 13 | logger.info.assert_called() 14 | -------------------------------------------------------------------------------- /devel/tests/test_reports.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TransactionTestCase 3 | 4 | 5 | class DeveloperReport(TransactionTestCase): 6 | fixtures = ['main/fixtures/arches.json', 'main/fixtures/repos.json', 7 | 'main/fixtures/package.json'] 8 | 9 | def setUp(self): 10 | password = 'test' 11 | self.user = User.objects.create_superuser('admin', 12 | 'admin@archlinux.org', 13 | password) 14 | self.client.post('/login/', { 15 | 'username': self.user.username, 16 | 'password': password 17 | }) 18 | 19 | def tearDown(self): 20 | self.user.delete() 21 | 22 | def test_overview(self): 23 | response = self.client.get('/devel/') 24 | self.assertEqual(response.status_code, 200) 25 | 26 | def test_reports_old(self): 27 | response = self.client.get('/devel/reports/old', follow=True) 28 | self.assertEqual(response.status_code, 200) 29 | 30 | def test_reports_outofdate(self): 31 | response = self.client.get('/devel/reports/long-out-of-date', follow=True) 32 | self.assertEqual(response.status_code, 200) 33 | 34 | def test_reports_big(self): 35 | response = self.client.get('/devel/reports/big', follow=True) 36 | self.assertEqual(response.status_code, 200) 37 | 38 | def test_reports_badcompression(self): 39 | response = self.client.get('/devel/reports/badcompression', follow=True) 40 | self.assertEqual(response.status_code, 200) 41 | 42 | def test_reports_uncompressed_man(self): 43 | response = self.client.get('/devel/reports/uncompressed-man', follow=True) 44 | self.assertEqual(response.status_code, 200) 45 | 46 | def test_reports_uncompressed_info(self): 47 | response = self.client.get('/devel/reports/uncompressed-info', follow=True) 48 | self.assertEqual(response.status_code, 200) 49 | 50 | def test_reports_unneeded_orphans(self): 51 | response = self.client.get('/devel/reports/unneeded-orphans', follow=True) 52 | self.assertEqual(response.status_code, 200) 53 | 54 | def test_reports_mismatched_signature(self): 55 | response = self.client.get('/devel/reports/mismatched-signature', follow=True) 56 | self.assertEqual(response.status_code, 200) 57 | 58 | def test_reports_signature_time(self): 59 | response = self.client.get('/devel/reports/signature-time', follow=True) 60 | self.assertEqual(response.status_code, 200) 61 | -------------------------------------------------------------------------------- /devel/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.test import TestCase 3 | 4 | from devel.templatetags.group import in_group 5 | 6 | 7 | class DevelTemplatetagsTest(TestCase): 8 | def test_in_group(self): 9 | user = User.objects.create(username="joeuser", first_name="Joe", 10 | last_name="User", email="user1@example.com") 11 | self.assertEqual(in_group(user, 'none'), False) 12 | -------------------------------------------------------------------------------- /devel/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from devel import views 4 | 5 | urlpatterns = [ 6 | path('admin_log/', views.admin_log), 7 | re_path(r'^admin_log/(?P.*)/$', views.admin_log), 8 | path('clock/', views.clock, name='devel-clocks'), 9 | path('tier0mirror/', views.tier0_mirror, name='tier0-mirror'), 10 | path('mirrorauth/', views.tier0_mirror_auth, name='tier0-mirror-atuh'), 11 | path('', views.index, name='devel-index'), 12 | path('stats/', views.stats, name='devel-stats'), 13 | path('newuser/', views.new_user_form), 14 | path('profile/', views.change_profile), 15 | re_path(r'^reports/(?P.*)/(?P.*)/$', views.report), 16 | re_path(r'^reports/(?P.*)/$', views.report), 17 | ] 18 | 19 | # vim: set ts=4 sw=4 et: 20 | -------------------------------------------------------------------------------- /docs/mirror_access.md: -------------------------------------------------------------------------------- 1 | # Mirror Access 2 | 3 | Archweb can be used as external authentication provider in combination with 4 | [ngx_http_auth_request_module](http://nginx.org/en/docs/http/ngx_http_auth_request_module.html). 5 | A user with a Developer, Package Maintainer and Support Staff role can generate an 6 | access token used in combination with his username on the `/devel/tier0mirror` 7 | url. The mirror authentication is done against `/devel/mirrorauth` using HTTP 8 | Basic authentication. 9 | 10 | ## Configuration 11 | 12 | There are two configuration options for this feature of which one is optional: 13 | 14 | * **TIER0_MIRROR_DOMAIN** - the mirror domain used to display the mirror url with authentication. 15 | * **TIER0_MIRROR_SECRET** - an optional secret send by nginx in the `X-Sent-From` header, all requests without this secret value are ignored. This can be used to not allow anyone to bruteforce guess the http basic auth pass/token. 16 | 17 | ## nginx configuration 18 | 19 | Example configuration with optional caching of the authentication request to 20 | reduce hammering archweb when for example using this feature for a mirror. By 21 | default archweb caches `/devel/mirrorauth` for 5 minutes. 22 | 23 | ``` 24 | http { 25 | proxy_cache_path /var/lib/nginx/cache/auth_cache levels=1:2 keys_zone=auth_cache:5m; 26 | 27 | server { 28 | location /protected { 29 | auth_request /devel/mirrorauth; 30 | 31 | root /usr/share/nginx/html; 32 | index index.html index.htm; 33 | } 34 | 35 | location = /devel/mirrorauth { 36 | internal; 37 | 38 | # Do not pass the request body, only http authorisation header is required 39 | proxy_pass_request_body off; 40 | proxy_set_header Content-Length ""; 41 | 42 | # Proxy headers 43 | proxy_set_header Host $host; 44 | proxy_set_header X-Original-URL $scheme://$http_host$request_uri; 45 | proxy_set_header X-Original-Method $request_method; 46 | proxy_set_header X-Auth-Request-Redirect $request_uri; 47 | proxy_set_header X-Sent-From "arch-nginx"; 48 | 49 | # Cache responses from the auth proxy 50 | proxy_cache auth_cache; 51 | proxy_cache_key "$scheme$proxy_host$request_uri$http_authorization"; 52 | 53 | # Authentication to archweb 54 | proxy_pass https://archlinux.org; 55 | } 56 | } 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /local_settings.py.example: -------------------------------------------------------------------------------- 1 | # Debug settings 2 | DEBUG = False 3 | # DEBUG_TOOLBAR = True 4 | 5 | # For django debug toolbar 6 | INTERNAL_IPS = ('127.0.0.1',) 7 | 8 | # Notification admins 9 | ADMINS = ( 10 | ('Joe Admin', 'joeadmin@example.com'), 11 | ) 12 | 13 | # PostgreSQL Database settings 14 | # DATABASES = { 15 | # 'default': { 16 | # 'ENGINE': 'django.db.backends.postgresql', 17 | # 'NAME': 'archlinux', 18 | # 'USER': 'archlinux', 19 | # 'PASSWORD': 'archlinux', 20 | # 'HOST': '', 21 | # 'PORT': '', 22 | # }, 23 | # } 24 | 25 | # Sqlite Database settings 26 | DATABASES = { 27 | 'default': { 28 | 'ENGINE': 'django.db.backends.sqlite3', 29 | 'NAME': 'database.db', 30 | }, 31 | } 32 | 33 | # Define cache settings 34 | CACHES = { 35 | 'default': { 36 | 'BACKEND': 'django.core.cache.backends.dummy.DummyCache', 37 | # 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 38 | # 'LOCATION': '127.0.0.1:11211', 39 | } 40 | } 41 | 42 | # Use secure session cookies? Make these True if you want all 43 | # logged-in actions to take place over HTTPS only. If developing 44 | # locally, you will want to use False. 45 | SESSION_COOKIE_SECURE = False 46 | CSRF_COOKIE_SECURE = False 47 | 48 | # location for saving dev pictures 49 | MEDIA_ROOT = '/srv/example.com/img/' 50 | 51 | # web url for serving image files 52 | MEDIA_URL = '/media/img/' 53 | 54 | # Make this unique, and don't share it with anybody. 55 | SECRET_KEY = '00000000000000000000000000000000000000000000000' 56 | 57 | # vim: set ts=4 sw=4 et: 58 | -------------------------------------------------------------------------------- /main/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/main/__init__.py -------------------------------------------------------------------------------- /main/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from main.models import Arch, Donor, Package, Repo 4 | 5 | 6 | class DonorAdmin(admin.ModelAdmin): 7 | list_display = ('name', 'visible', 'created') 8 | list_filter = ('visible', 'created') 9 | search_fields = ('name',) 10 | exclude = ('created',) 11 | 12 | 13 | class ArchAdmin(admin.ModelAdmin): 14 | list_display = ('name', 'agnostic', 'required_signoffs') 15 | list_filter = ('agnostic',) 16 | search_fields = ('name',) 17 | 18 | 19 | class RepoAdmin(admin.ModelAdmin): 20 | list_display = ('name', 'testing', 'staging', 'bugs_project', 'bugs_category', 'svn_root') 21 | list_filter = ('testing', 'staging') 22 | search_fields = ('name',) 23 | 24 | 25 | class PackageAdmin(admin.ModelAdmin): 26 | list_display = ('pkgname', 'full_version', 'repo', 'arch', 'packager', 'last_update', 'build_date') 27 | list_filter = ('repo', 'arch') 28 | search_fields = ('pkgname', 'pkgbase', 'pkgdesc') 29 | date_hierarchy = 'build_date' 30 | 31 | 32 | admin.site.register(Donor, DonorAdmin) 33 | 34 | admin.site.register(Package, PackageAdmin) 35 | admin.site.register(Arch, ArchAdmin) 36 | admin.site.register(Repo, RepoAdmin) 37 | 38 | # vim: set ts=4 sw=4 et: 39 | -------------------------------------------------------------------------------- /main/context_processors.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | 4 | def mastodon_link(request): 5 | return {'MASTODON_LINK': getattr(settings, "MASTODON_LINK", "")} 6 | -------------------------------------------------------------------------------- /main/fixtures/arches.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "main.arch", 5 | "fields": { 6 | "agnostic": true, 7 | "name": "any", 8 | "required_signoffs": 2 9 | } 10 | }, 11 | { 12 | "pk": 2, 13 | "model": "main.arch", 14 | "fields": { 15 | "agnostic": false, 16 | "name": "i686", 17 | "required_signoffs": 1 18 | } 19 | }, 20 | { 21 | "pk": 3, 22 | "model": "main.arch", 23 | "fields": { 24 | "agnostic": false, 25 | "name": "x86_64", 26 | "required_signoffs": 2 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /main/fixtures/denylist.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "packages.flagdenylist", 4 | "pk": 1, 5 | "fields": { 6 | "keyword": "bit.ly" 7 | } 8 | } 9 | ] -------------------------------------------------------------------------------- /main/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/main/management/__init__.py -------------------------------------------------------------------------------- /main/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/main/management/commands/__init__.py -------------------------------------------------------------------------------- /main/migrations/0002_repo_public_testing.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-04-28 14:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('main', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='repo', 15 | name='public_testing', 16 | field=models.BooleanField(default=False, help_text='Is this repo meant for package testing (without signoffs)?'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /main/migrations/0003_rebuilderdstatus.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-28 19:34 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('main', '0002_repo_public_testing'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='RebuilderdStatus', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('pkgname', models.CharField(max_length=255)), 19 | ('pkgver', models.CharField(max_length=255)), 20 | ('pkgrel', models.CharField(max_length=255)), 21 | ('epoch', models.PositiveIntegerField(default=0)), 22 | ('status', models.SmallIntegerField(choices=[(0, 'Good'), (1, 'Bad'), (2, 'Unknown')], default=2)), 23 | ('was_repro', models.BooleanField(default=False)), 24 | ('arch', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Arch')), 25 | ('pkg', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Package')), 26 | ('repo', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.Repo')), 27 | ], 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /main/migrations/0004_rebuilderdstatus_build_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-01-31 16:14 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('main', '0003_rebuilderdstatus'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='rebuilderdstatus', 15 | name='build_id', 16 | field=models.IntegerField(blank=True, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /main/migrations/0004_soname.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.3 on 2020-11-03 20:33 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('main', '0003_rebuilderdstatus'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Soname', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=255)), 19 | ('pkg', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='main.package')), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /main/migrations/0005_merge_0004_rebuilderdstatus_build_id_0004_soname.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2 on 2021-04-28 15:11 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('main', '0004_rebuilderdstatus_build_id'), 10 | ('main', '0004_soname'), 11 | ] 12 | 13 | operations = [ 14 | ] 15 | -------------------------------------------------------------------------------- /main/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/main/migrations/__init__.py -------------------------------------------------------------------------------- /main/storage.py: -------------------------------------------------------------------------------- 1 | import jsmin 2 | from django.contrib.staticfiles.storage import ManifestStaticFilesStorage 3 | from django.core.files.base import ContentFile 4 | from django.utils.encoding import smart_str 5 | 6 | import cssmin 7 | 8 | 9 | class MinifiedStaticFilesStorage(ManifestStaticFilesStorage): 10 | """ 11 | A static file system storage backend which minifies the hashed 12 | copies of the files it saves. It currently knows how to process 13 | CSS and JS files. Files containing '.min' anywhere in the filename 14 | are skipped as they are already assumed minified. 15 | """ 16 | minifiers = ( 17 | ('.css', cssmin.cssmin), 18 | ('.js', jsmin.jsmin), 19 | ) 20 | 21 | def post_process(self, paths, dry_run=False, **options): 22 | for original_path, processed_path, processed in super( 23 | MinifiedStaticFilesStorage, self).post_process( 24 | paths, dry_run, **options): 25 | for ext, func in self.minifiers: 26 | if '.min' in original_path: 27 | continue 28 | if original_path.endswith(ext): 29 | with self._open(processed_path) as processed_file: 30 | minified = func(processed_file.read().decode('utf-8')) 31 | minified_file = ContentFile(smart_str(minified)) 32 | self.delete(processed_path) 33 | self._save(processed_path, minified_file) 34 | processed = True 35 | 36 | yield original_path, processed_path, processed 37 | -------------------------------------------------------------------------------- /main/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/main/templatetags/__init__.py -------------------------------------------------------------------------------- /main/templatetags/attributes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django import template 4 | from django.conf import settings 5 | 6 | numeric_test = re.compile(r"^\d+$") 7 | register = template.Library() 8 | 9 | 10 | def attribute(value, arg): 11 | """Gets an attribute of an object dynamically from a string name""" 12 | if hasattr(value, str(arg)): 13 | return getattr(value, arg) 14 | elif numeric_test.match(str(arg)) and len(value) > int(arg): 15 | return value[int(arg)] 16 | else: 17 | return settings.TEMPLATE_STRING_IF_INVALID 18 | 19 | 20 | register.filter('attribute', attribute) 21 | 22 | # vim: set ts=4 sw=4 et: 23 | -------------------------------------------------------------------------------- /main/templatetags/cdn.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.contrib.staticfiles.storage import staticfiles_storage 3 | from django.utils.html import format_html 4 | from django.utils.safestring import mark_safe 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.simple_tag 10 | def jquery(): 11 | version = '3.6.0' 12 | filename = 'jquery-%s.min.js' % version 13 | link = staticfiles_storage.url(filename) 14 | return mark_safe('' % link) 15 | 16 | 17 | @register.simple_tag 18 | def jquery_tablesorter(): 19 | version = '2.31.0' 20 | filename = 'jquery.tablesorter-%s.min.js' % version 21 | link = staticfiles_storage.url(filename) 22 | return format_html('' % link) 23 | 24 | 25 | @register.simple_tag 26 | def d3js(): 27 | version = '3.5.0' 28 | filename = 'd3-%s.min.js' % version 29 | link = staticfiles_storage.url(filename) 30 | return format_html('' % link) 31 | 32 | # vim: set ts=4 sw=4 et: 33 | -------------------------------------------------------------------------------- /main/templatetags/flags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.html import format_html 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.simple_tag 8 | def country_flag(country): 9 | if not country: 10 | return '' 11 | return format_html(' ' % ( 12 | str(country.code).lower(), str(country.name))) 13 | 14 | 15 | # vim: set ts=4 sw=4 et: 16 | -------------------------------------------------------------------------------- /main/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/main/tests/__init__.py -------------------------------------------------------------------------------- /main/tests/test_donor_import.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from email.header import Header 4 | from email.message import Message 5 | from io import StringIO 6 | 7 | import pytest 8 | from django.core.management import call_command 9 | from django.core.management.base import CommandError 10 | 11 | from main.management.commands.donor_import import Command 12 | from main.models import Donor 13 | 14 | command = Command() 15 | 16 | 17 | def test_parse_subject(): 18 | assert command.parse_subject('garbage') is None 19 | 20 | # Valid 21 | valid = 'Receipt [$25.00] By: John Doe [john.doe@archlinux.org]' 22 | output = command.parse_subject(valid) 23 | assert output == 'John Doe' 24 | 25 | 26 | def test_parse_name(): 27 | assert command.sanitize_name('1244') == '' 28 | assert command.sanitize_name('John Doe') == 'John Doe' 29 | assert command.sanitize_name(' John Doe ') == 'John Doe' 30 | assert command.sanitize_name('John Doe 23') == 'John Doe' 31 | 32 | 33 | def test_decode_subject(): 34 | text = u'メイル' 35 | subject = Header(text, 'utf-8') 36 | assert command.decode_subject(subject) == text 37 | 38 | 39 | def test_invalid_args(monkeypatch): 40 | monkeypatch.setattr('sys.stdin', StringIO('')) 41 | with pytest.raises(CommandError) as e: 42 | call_command('donor_import') 43 | assert 'Failed to read from STDIN' in str(e.value) 44 | 45 | 46 | def test_invalid_path(): 47 | with pytest.raises(CommandError) as e: 48 | call_command('donor_import', '/tmp/non-existant') 49 | assert 'argument input: can\'t open' in str(e.value) 50 | 51 | 52 | def test_maildir(db, monkeypatch): 53 | msg = Message() 54 | msg['subject'] = 'John Doe' 55 | msg['to'] = 'John Doe ' 56 | 57 | # Invalid 58 | monkeypatch.setattr('sys.stdin', StringIO(msg.as_string())) 59 | with pytest.raises(SystemExit): 60 | call_command('donor_import') 61 | assert len(Donor.objects.all()) == 0 62 | 63 | # # Valid 64 | msg = Message() 65 | msg['subject'] = 'Receipt [$25.00] By: David Doe [david@doe.com]' 66 | msg['to'] = 'John Doe ' 67 | monkeypatch.setattr('sys.stdin', StringIO(msg.as_string())) 68 | call_command('donor_import') 69 | assert len(Donor.objects.all()) == 1 70 | 71 | # # Re-running should result in no new donor 72 | monkeypatch.setattr('sys.stdin', StringIO(msg.as_string())) 73 | call_command('donor_import') 74 | assert len(Donor.objects.all()) == 1 75 | -------------------------------------------------------------------------------- /main/tests/test_templatetags_flags.py: -------------------------------------------------------------------------------- 1 | from main.templatetags.flags import country_flag 2 | from mirrors.tests.conftest import checklocation # noqa: F401 3 | 4 | 5 | def test_country_flag(checklocation): # noqa: F811 6 | flag = country_flag(checklocation.country) 7 | assert checklocation.country.name in flag 8 | assert checklocation.country.code.lower() in flag 9 | assert country_flag(None) == '' 10 | -------------------------------------------------------------------------------- /main/tests/test_templatetags_pgp.py: -------------------------------------------------------------------------------- 1 | from main.templatetags.pgp import format_key, pgp_fingerprint, pgp_key_link 2 | 3 | 4 | def test_format_key(): 5 | # 40 len case 6 | pgp_key = '423423fD9004FB063E2C81117BFB1108D234DAFZ' 7 | pgp_key_len = len(pgp_key) 8 | 9 | output = format_key(pgp_key) 10 | spaces = output.count(' ') + output.count('\xa0') # nbsp 11 | assert pgp_key_len + spaces == len(output) 12 | 13 | # 21 - 39 len case 14 | pgp_key = '3E2C81117BFB1108D234DAFZ' 15 | pgp_key_len = len(pgp_key) + len('0x') 16 | assert pgp_key_len == len(format_key(pgp_key)) 17 | 18 | # 8, 20 len case 19 | pgp_key = '3E2C81117BFB1108DEFF' 20 | pgp_key_len = len(pgp_key) + len('0x') 21 | assert pgp_key_len == len(format_key(pgp_key)) 22 | 23 | # 0 - 7 len case 24 | pgp_key = 'B1108D' 25 | pgp_key_len = len(pgp_key) + len('0x') 26 | assert pgp_key_len == len(format_key(pgp_key)) 27 | 28 | 29 | def assert_pgp_key_link(pgp_key): 30 | output = pgp_key_link(int(pgp_key, 16)) 31 | assert pgp_key[2:] in output 32 | assert "https" in output 33 | 34 | 35 | def test_pgp_key_link(settings): 36 | assert pgp_key_link("") == "Unknown" 37 | 38 | pgp_key = '423423fD9004FB063E2C81117BFB1108D234DAFZ' 39 | output = pgp_key_link(pgp_key) 40 | assert pgp_key in output 41 | assert "https" in output 42 | 43 | output = pgp_key_link(pgp_key, "test") 44 | assert "test" in output 45 | assert "https" in output 46 | 47 | # Numeric key_id <= 8 48 | assert_pgp_key_link('0x0023BDC7') 49 | 50 | # Numeric key_id <= 16 51 | assert_pgp_key_link('0xBDC7FF5E34A12F') 52 | 53 | # Numeric key_id <= 40 54 | assert_pgp_key_link('0xA10E234343EA8BDC7FF5E34A12F') 55 | 56 | pgp_key = '423423fD9004FB063E2C81117BFB1108D234DAFZ' 57 | server = settings.PGP_SERVER 58 | 59 | settings.PGP_SERVER = '' 60 | assert server not in pgp_key_link(pgp_key) 61 | 62 | settings.PGP_SERVER_SECURE = False 63 | pgp_key = '423423fD9004FB063E2C81117BFB1108D234DAFZ' 64 | assert "https" not in pgp_key_link(pgp_key) 65 | 66 | 67 | def test_pgp_fingerprint(): 68 | assert pgp_fingerprint(None) == "" 69 | keyid = '423423fD9004FB063E2C81117BFB1108D234DAFZ' 70 | fingerprint = pgp_fingerprint(keyid) 71 | assert len(fingerprint) > len(keyid) 72 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | 12 | # vim: set ts=4 sw=4 et: 13 | -------------------------------------------------------------------------------- /mirrors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/mirrors/__init__.py -------------------------------------------------------------------------------- /mirrors/fields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.core import validators 3 | from django.core.exceptions import ValidationError 4 | from django.db import models 5 | from IPy import IP 6 | 7 | 8 | class IPNetworkFormField(forms.Field): 9 | def to_python(self, value): 10 | if value in validators.EMPTY_VALUES: 11 | return None 12 | try: 13 | value = IP(value) 14 | except ValueError as e: 15 | raise ValidationError(str(e)) 16 | return value 17 | 18 | 19 | class IPNetworkField(models.Field): 20 | description = "IPv4 or IPv6 address or subnet" 21 | 22 | def __init__(self, *args, **kwargs): 23 | kwargs['max_length'] = 44 24 | super(IPNetworkField, self).__init__(*args, **kwargs) 25 | 26 | def get_internal_type(self): 27 | return "IPAddressField" 28 | 29 | def to_python(self, value): 30 | if not value: 31 | return None 32 | return IP(value) 33 | 34 | def get_prep_value(self, value): 35 | value = self.to_python(value) 36 | if not value: 37 | return None 38 | return str(value) 39 | 40 | def formfield(self, **kwargs): 41 | defaults = {'form_class': IPNetworkFormField} 42 | defaults.update(kwargs) 43 | return super(IPNetworkField, self).formfield(**defaults) 44 | 45 | def from_db_value(self, value, expression, connection): 46 | return self.to_python(value) 47 | -------------------------------------------------------------------------------- /mirrors/fixtures/mirrorprotocols.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "mirrors.mirrorprotocol", 5 | "fields": { 6 | "is_download": true, 7 | "default": true, 8 | "protocol": "http" 9 | } 10 | }, 11 | { 12 | "pk": 3, 13 | "model": "mirrors.mirrorprotocol", 14 | "fields": { 15 | "is_download": false, 16 | "default": false, 17 | "protocol": "rsync" 18 | } 19 | }, 20 | { 21 | "pk": 5, 22 | "model": "mirrors.mirrorprotocol", 23 | "fields": { 24 | "is_download": true, 25 | "default": false, 26 | "protocol": "https" 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /mirrors/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/mirrors/management/__init__.py -------------------------------------------------------------------------------- /mirrors/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/mirrors/management/commands/__init__.py -------------------------------------------------------------------------------- /mirrors/management/commands/mirrorresolv.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | mirrorresolv command 4 | 5 | Poll all mirror URLs and determine whether they have IPv4 and/or IPv6 addresses 6 | available. 7 | 8 | Usage: ./manage.py mirrorresolv 9 | """ 10 | 11 | import logging 12 | import socket 13 | 14 | from django.core.management.base import BaseCommand 15 | 16 | from mirrors.models import MirrorUrl 17 | 18 | logger = logging.getLogger("command") 19 | logger.setLevel(logging.WARNING) 20 | 21 | 22 | class Command(BaseCommand): 23 | help = "Runs a check on all active mirror URLs to determine if they are reachable via IPv4 and/or v6." 24 | 25 | def handle(self, **options): 26 | v = int(options.get('verbosity', 0)) 27 | if v == 0: 28 | logger.level = logging.ERROR 29 | elif v == 1: 30 | logger.level = logging.WARNING 31 | elif v >= 2: 32 | logger.level = logging.DEBUG 33 | 34 | return resolve_mirrors() 35 | 36 | 37 | def resolve_mirrors(): 38 | logger.debug("requesting list of mirror URLs") 39 | for mirrorurl in MirrorUrl.objects.filter(active=True, mirror__active=True): 40 | try: 41 | # save old values, we can skip no-op updates this way 42 | oldvals = (mirrorurl.has_ipv4, mirrorurl.has_ipv6) 43 | logger.debug("resolving %3i (%s)", mirrorurl.id, mirrorurl.hostname) 44 | families = mirrorurl.address_families() 45 | mirrorurl.has_ipv4 = socket.AF_INET in families 46 | mirrorurl.has_ipv6 = socket.AF_INET6 in families 47 | logger.debug("%s: v4: %s v6: %s", mirrorurl.hostname, 48 | mirrorurl.has_ipv4, mirrorurl.has_ipv6) 49 | # now check new values, only update if new != old 50 | newvals = (mirrorurl.has_ipv4, mirrorurl.has_ipv6) 51 | if newvals != oldvals: 52 | logger.debug("values changed for %s", mirrorurl) 53 | mirrorurl.save(update_fields=('has_ipv4', 'has_ipv6')) 54 | except socket.gaierror as e: 55 | if e.errno == socket.EAI_NONAME: 56 | logger.debug("gaierror resolving %s: %s", mirrorurl.hostname, e) 57 | else: 58 | logger.warning("gaierror resolving %s: %s", mirrorurl.hostname, e) 59 | except socket.error as e: 60 | logger.warning("error resolving %s: %s", mirrorurl.hostname, e) 61 | 62 | # vim: set ts=4 sw=4 et: 63 | -------------------------------------------------------------------------------- /mirrors/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/mirrors/migrations/__init__.py -------------------------------------------------------------------------------- /mirrors/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/mirrors/templatetags/__init__.py -------------------------------------------------------------------------------- /mirrors/templatetags/mirror_status.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django import template 4 | 5 | register = template.Library() 6 | 7 | 8 | @register.filter 9 | def duration(value): 10 | if not value or not isinstance(value, timedelta): 11 | return u'' 12 | # does not take microseconds into account 13 | total_secs = value.seconds + value.days * 24 * 3600 14 | mins = total_secs // 60 15 | hrs, mins = divmod(mins, 60) 16 | return '%d:%02d' % (hrs, mins) 17 | 18 | 19 | @register.filter 20 | def hours(value): 21 | if not value or not isinstance(value, timedelta): 22 | return u'' 23 | # does not take microseconds into account 24 | total_secs = value.seconds + value.days * 24 * 3600 25 | mins = total_secs // 60 26 | hrs, mins = divmod(mins, 60) 27 | if hrs == 1: 28 | return '%d hour' % hrs 29 | return '%d hours' % hrs 30 | 31 | 32 | @register.filter 33 | def percentage(value, arg=1): 34 | if not value or not isinstance(value, float): 35 | return u'' 36 | new_val = value * 100.0 37 | return '%.*f%%' % (arg, new_val) 38 | 39 | # vim: set ts=4 sw=4 et: 40 | -------------------------------------------------------------------------------- /mirrors/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/mirrors/tests/__init__.py -------------------------------------------------------------------------------- /mirrors/tests/test_mirrorlist.py: -------------------------------------------------------------------------------- 1 | from mirrors.models import Mirror 2 | 3 | # TODO(jelle): add test for https/rsync mirrors 4 | 5 | 6 | def test_mirrorlist(client, mirrorurl): 7 | response = client.get('/mirrorlist/') 8 | assert response.status_code == 200 9 | 10 | 11 | def test_mirrorlist_tier_last(client, mirrorurl): 12 | last_tier = Mirror.TIER_CHOICES[-1][0] 13 | response = client.get(f'/mirrorlist/tier/{last_tier + 1}/') 14 | assert response.status_code == 404 15 | 16 | 17 | def test_mirrorlist_all(client, mirrorurl): 18 | response = client.get('/mirrorlist/all/') 19 | assert response.status_code == 200 20 | assert mirrorurl.hostname in response.content.decode() 21 | 22 | 23 | def test_mirrorlist_all_https(client, mirrorurl): 24 | response = client.get('/mirrorlist/all/https/') 25 | assert response.status_code == 200 26 | assert mirrorurl.hostname in response.content.decode() 27 | 28 | 29 | def test_mirrorlist_all_http(client, mirrorurl): 30 | # First test that without any http mirrors, we get a 404. 31 | response = client.get('/mirrorlist/all/http/') 32 | assert response.status_code == 404 33 | 34 | 35 | def test_mirrorlist_status(client, mirrorurl): 36 | response = client.get('/mirrorlist/?country=all&use_mirror_status=on') 37 | assert response.status_code == 200 38 | 39 | 40 | def test_mirrorlist_filter(client, create_mirrorurl): 41 | mirror1 = create_mirrorurl('JP', 'https://jp.org') 42 | mirror2 = create_mirrorurl() 43 | 44 | # First test that we correctly see the above mirror. 45 | response = client.get('/mirrorlist/?country=JP&protocol=https') 46 | assert response.status_code == 200 47 | assert mirror1.hostname in response.content.decode() 48 | 49 | # Now confirm that the US mirror did not show up. 50 | assert mirror2.hostname not in response.content.decode() 51 | -------------------------------------------------------------------------------- /mirrors/tests/test_mirrorlocations.py: -------------------------------------------------------------------------------- 1 | from mirrors.tests.conftest import COUNTRY 2 | 3 | 4 | def test_mirrorlocations_json(client, checklocation): 5 | response = client.get('/mirrors/locations/json/') 6 | assert response.status_code == 200 7 | data = response.json() 8 | assert 1 == data['version'] 9 | location = data['locations'][0]['country_code'] 10 | assert COUNTRY == location 11 | -------------------------------------------------------------------------------- /mirrors/tests/test_mirrorresolv.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from django.core.management import call_command 4 | 5 | 6 | @mock.patch('socket.getaddrinfo') 7 | def test_ip4_ip6(getaddrinfo, db, mirrorurl): 8 | getaddrinfo.return_value = [(2, 1, 6, '', ('1.1.1.1', 0)), (10, 1, 6, '', ('1a01:3f8:132:1d96::1', 0, 0, 0))] 9 | call_command('mirrorresolv') 10 | mirrorurl.refresh_from_db() 11 | 12 | assert mirrorurl.has_ipv4 13 | assert mirrorurl.has_ipv6 14 | 15 | 16 | @mock.patch('socket.getaddrinfo') 17 | def test_ip4_only(getaddrinfo, db, mirrorurl): 18 | getaddrinfo.return_value = [(2, 1, 6, '', ('1.1.1.1', 0))] 19 | call_command('mirrorresolv') 20 | mirrorurl.refresh_from_db() 21 | 22 | assert mirrorurl.has_ipv4 23 | assert not mirrorurl.has_ipv6 24 | 25 | 26 | @mock.patch('socket.getaddrinfo') 27 | def test_running_twice(getaddrinfo, db, mirrorurl): 28 | getaddrinfo.return_value = [(2, 1, 6, '', ('1.1.1.1', 0)), (10, 1, 6, '', ('1a01:3f8:132:1d96::1', 0, 0, 0))] 29 | 30 | # Check if values changed 31 | with mock.patch('mirrors.management.commands.mirrorresolv.logger') as logger: 32 | call_command('mirrorresolv', '-v3') 33 | assert logger.debug.call_count == 4 34 | 35 | # running again does not change any values. 36 | with mock.patch('mirrors.management.commands.mirrorresolv.logger') as logger: 37 | call_command('mirrorresolv', '-v3') 38 | assert logger.debug.call_count == 3 39 | -------------------------------------------------------------------------------- /mirrors/tests/test_mirrorrsync.py: -------------------------------------------------------------------------------- 1 | from django.test import TransactionTestCase 2 | 3 | from mirrors.models import Mirror, MirrorRsync 4 | 5 | TEST_IPV6 = "2a0b:4342:1a31:410::" 6 | TEST_IPV4 = "8.8.8.8" 7 | 8 | 9 | class MirrorRsyncTest(TransactionTestCase): 10 | def setUp(self): 11 | self.mirror = Mirror.objects.create(name='rmirror', 12 | admin_email='foo@bar.com') 13 | 14 | def tearDown(self): 15 | self.mirror.delete() 16 | 17 | def test_ipv6(self): 18 | mirrorrsync = MirrorRsync.objects.create(ip=TEST_IPV6, mirror=self.mirror) 19 | self.assertEqual(str(mirrorrsync), TEST_IPV6) 20 | mirrorrsync.delete() 21 | 22 | def test_ipv4(self): 23 | mirrorrsync = MirrorRsync.objects.create(ip=TEST_IPV4, mirror=self.mirror) 24 | self.assertEqual(str(mirrorrsync), TEST_IPV4) 25 | mirrorrsync.delete() 26 | 27 | def test_invalid(self): 28 | with self.assertRaises(ValueError) as e: 29 | MirrorRsync.objects.create(ip="8.8.8.8.8", mirror=self.mirror) 30 | self.assertIn('IPv4 Address with more than 4 bytes', str(e.exception)) 31 | -------------------------------------------------------------------------------- /mirrors/tests/test_mirrors.py: -------------------------------------------------------------------------------- 1 | from mirrors.models import MirrorUrl 2 | 3 | 4 | def test_details_empty(db, client): 5 | response = client.get('/mirrors/nothing/') 6 | assert response.status_code == 404 7 | 8 | 9 | def test_details(db, client, mirrorurl): 10 | url = mirrorurl.mirror.get_absolute_url() 11 | 12 | response = client.get(url) 13 | assert response.status_code == 200 14 | 15 | 16 | def test_details_json_empty(db, client): 17 | response = client.get('/mirrors/nothing/json/') 18 | assert response.status_code == 404 19 | 20 | 21 | def test_details_json(db, client, mirrorurl): 22 | url = mirrorurl.mirror.get_absolute_url() 23 | 24 | response = client.get(url + 'json/') 25 | assert response.status_code == 200 26 | data = response.json() 27 | assert data['urls'] != [] 28 | assert data['tier'] == 0 29 | assert 'upstream' not in data 30 | 31 | 32 | def test_details_downstream_json(db, client, downstream_mirror, mirrorprotocol): 33 | mirror_url = MirrorUrl.objects.create(url="https://example.org/arch/mirror", 34 | protocol=mirrorprotocol, 35 | mirror=downstream_mirror, 36 | country="DE") 37 | 38 | url = mirror_url.mirror.get_absolute_url() 39 | response = client.get(url + 'json/') 40 | assert response.status_code == 200 41 | data = response.json() 42 | assert data['urls'] != [] 43 | assert data['tier'] == 2 44 | assert data['upstream'] == 'mirror1' 45 | 46 | mirror_url.delete() 47 | 48 | 49 | def test_url_details(db, client, mirrorurl): 50 | url = mirrorurl.mirror.get_absolute_url() 51 | response = client.get(url + f'{mirrorurl.id}/') 52 | assert response.status_code == 200 53 | -------------------------------------------------------------------------------- /mirrors/tests/test_mirrorurl.py: -------------------------------------------------------------------------------- 1 | from mirrors.tests.conftest import HOSTNAME, URL 2 | 3 | 4 | def test_mirrorurl_address_families(mirrorurl): 5 | assert mirrorurl.address_families() is not None 6 | 7 | 8 | def test_mirrorurl_hostname(mirrorurl): 9 | assert mirrorurl.hostname == HOSTNAME 10 | 11 | 12 | def test_mirrorurl_get_absolute_url(mirrorurl): 13 | absolute_url = mirrorurl.get_absolute_url() 14 | expected = '/mirrors/%s/%d/' % (mirrorurl.mirror.name, mirrorurl.pk) 15 | assert absolute_url == expected 16 | 17 | 18 | def test_mirrorurl_overview(client, mirrorurl): 19 | response = client.get('/mirrors/') 20 | assert response.status_code == 200 21 | assert mirrorurl.mirror.name in response.content.decode() 22 | 23 | 24 | def test_mirrorurl_get_full_url(mirrorurl): 25 | assert f'mirrors/{mirrorurl.mirror.name}' in mirrorurl.get_full_url() 26 | 27 | 28 | def test_mirror_url_clean(mirrorurl): 29 | mirrorurl.clean() 30 | # TODO(jelle): this expects HOSTNAME to resolve, maybe mock 31 | assert mirrorurl.has_ipv4 32 | # requires ipv6 on host... mock? 33 | # assert mirrorurl.has_ipv6 == True 34 | 35 | 36 | def test_mirrorurl_repr(mirrorurl): 37 | assert URL in repr(mirrorurl) 38 | -------------------------------------------------------------------------------- /mirrors/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from mirrors.tests.conftest import HOSTNAME, NAME, PROTOCOL 2 | 3 | 4 | def test_mirror_get_full_url(mirror): 5 | assert mirror.get_absolute_url() in mirror.get_full_url() 6 | assert 'http' in mirror.get_full_url('http') 7 | 8 | 9 | def test_mirror_downstream(mirror): 10 | assert list(mirror.downstream()) == [] 11 | 12 | 13 | def test_mirror_get_absolute_url(mirror): 14 | absolute_url = mirror.get_absolute_url() 15 | expected = f'/mirrors/{mirror.name}/' 16 | assert absolute_url == expected 17 | 18 | 19 | def test_mirror_rer(mirror): 20 | assert NAME in repr(mirror) 21 | 22 | 23 | def test_checklocation_family(checklocation): 24 | assert isinstance(checklocation.family, int) 25 | 26 | 27 | def test_checklocation_ip_version(checklocation): 28 | assert isinstance(checklocation.ip_version, int) 29 | 30 | 31 | def test_checklocation_repr(checklocation): 32 | assert HOSTNAME in repr(checklocation) 33 | 34 | 35 | def test_mirrorprotocol_repr(mirrorprotocol): 36 | assert PROTOCOL in repr(mirrorprotocol) 37 | -------------------------------------------------------------------------------- /mirrors/tests/test_templatetags.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from mirrors.templatetags.mirror_status import duration, hours, percentage 4 | 5 | 6 | def test_duration(): 7 | assert duration(None) == '' 8 | 9 | assert duration(timedelta(hours=5)) == '5:00' 10 | assert duration(timedelta(hours=5, seconds=61)) == '5:01' 11 | # Microseconds are skipped 12 | assert duration(timedelta(microseconds=9999)) == '0:00' 13 | 14 | 15 | def test_hours(): 16 | assert hours(None) == '' 17 | 18 | assert hours(timedelta(hours=5)) == '5 hours' 19 | assert hours(timedelta(hours=1)) == '1 hour' 20 | assert hours(timedelta(seconds=60 * 60)) == '1 hour' 21 | 22 | 23 | def test_percentage(): 24 | assert percentage(None) == '' 25 | assert percentage(10.0) == '1000.0%' 26 | assert percentage(10.0, 2) == '1000.00%' 27 | -------------------------------------------------------------------------------- /mirrors/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | from django.views.decorators.cache import cache_page 3 | 4 | from .views import mirror_details, mirrors, status, url_details 5 | from .views.api import locations_json, mirror_details_json, status_json 6 | 7 | urlpatterns = [ 8 | path('', mirrors, name='mirror-list'), 9 | re_path(r'^tier/(?P\d+)/$', mirrors, name='mirror-list-tier'), 10 | path('status/', status, name='mirror-status'), 11 | path('status/json/', status_json, name='mirror-status-json'), 12 | re_path(r'^status/tier/(?P\d+)/$', status, name='mirror-status-tier'), 13 | re_path(r'^status/tier/(?P\d+)/json/$', status_json, name='mirror-status-tier-json'), 14 | path('locations/json/', cache_page(317)(locations_json), name='mirror-locations-json'), 15 | re_path(r'^(?P[\.\-\w]+)/$', mirror_details), 16 | re_path(r'^(?P[\.\-\w]+)/json/$', mirror_details_json), 17 | re_path(r'^(?P[\.\-\w]+)/(?P\d+)/$', url_details), 18 | ] 19 | 20 | # vim: set ts=4 sw=4 et: 21 | -------------------------------------------------------------------------------- /mirrors/urls_mirrorlist.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from mirrors.views import mirrorlist as views 4 | 5 | urlpatterns = [ 6 | path('', views.generate_mirrorlist, name='mirrorlist'), 7 | re_path(r'^all/$', views.find_mirrors, {'countries': ['all']}), 8 | re_path(r'^all/(?P[A-z]+)/$', views.find_mirrors_simple, name='mirrorlist_simple') 9 | ] 10 | 11 | # vim: set ts=4 sw=4 et: 12 | -------------------------------------------------------------------------------- /news/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/news/__init__.py -------------------------------------------------------------------------------- /news/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import News 4 | 5 | 6 | class NewsAdmin(admin.ModelAdmin): 7 | list_display = ('title', 'author', 'postdate', 'last_modified', 'safe_mode') 8 | list_filter = ('postdate', 'author', 'safe_mode') 9 | search_fields = ('title', 'content') 10 | date_hierarchy = 'postdate' 11 | 12 | 13 | admin.site.register(News, NewsAdmin) 14 | 15 | # vim: set ts=4 sw=4 et: 16 | -------------------------------------------------------------------------------- /news/migrations/0001_squashed_0002_news_send_announce.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.15 on 2018-11-17 20:55 3 | from __future__ import unicode_literals 4 | 5 | import django.db.models.deletion 6 | from django.conf import settings 7 | from django.db import migrations, models 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='News', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('slug', models.SlugField(max_length=255, unique=True)), 24 | ('postdate', models.DateTimeField(db_index=True, verbose_name='post date')), 25 | ('last_modified', models.DateTimeField(db_index=True, editable=False)), 26 | ('title', models.CharField(max_length=255)), 27 | ('guid', models.CharField(editable=False, max_length=255)), 28 | ('content', models.TextField()), 29 | ('safe_mode', models.BooleanField(default=True)), 30 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='news_author', to=settings.AUTH_USER_MODEL)), 31 | ('send_announce', models.BooleanField(default=True)), 32 | ], 33 | options={ 34 | 'ordering': ('-postdate',), 35 | 'db_table': 'news', 36 | 'verbose_name_plural': 'news', 37 | 'get_latest_by': 'postdate', 38 | }, 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /news/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/news/migrations/__init__.py -------------------------------------------------------------------------------- /news/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.contrib.sites.models import Site 3 | from django.db import models 4 | from django.db.models.signals import pre_save 5 | from django.utils.safestring import mark_safe 6 | from django.utils.timezone import now 7 | 8 | from main.utils import parse_markdown 9 | 10 | 11 | class News(models.Model): 12 | slug = models.SlugField(max_length=255, unique=True) 13 | author = models.ForeignKey(User, related_name='news_author', 14 | on_delete=models.PROTECT) 15 | postdate = models.DateTimeField("post date", db_index=True) 16 | last_modified = models.DateTimeField(editable=False, db_index=True) 17 | title = models.CharField(max_length=255) 18 | guid = models.CharField(max_length=255, editable=False) 19 | content = models.TextField() 20 | safe_mode = models.BooleanField(default=True) 21 | send_announce = models.BooleanField(default=True) 22 | 23 | def get_absolute_url(self): 24 | return '/news/%s/' % self.slug 25 | 26 | def html(self): 27 | return mark_safe(parse_markdown(self.content, not self.safe_mode)) 28 | 29 | def __str__(self): 30 | return self.title 31 | 32 | class Meta: 33 | db_table = 'news' 34 | verbose_name_plural = 'news' 35 | get_latest_by = 'postdate' 36 | ordering = ('-postdate',) 37 | 38 | 39 | def set_news_fields(sender, **kwargs): 40 | news = kwargs['instance'] 41 | current_time = now() 42 | news.last_modified = current_time 43 | if not news.postdate: 44 | news.postdate = current_time 45 | # http://diveintomark.org/archives/2004/05/28/howto-atom-id 46 | news.guid = f"tag:{Site.objects.get_current()},{current_time.strftime('%Y-%m-%d')}:{news.get_absolute_url()}" 47 | 48 | 49 | pre_save.connect(set_news_fields, sender=News, dispatch_uid="news.models") 50 | 51 | # vim: set ts=4 sw=4 et: 52 | -------------------------------------------------------------------------------- /news/tests/test_crud.py: -------------------------------------------------------------------------------- 1 | from django.core import mail 2 | 3 | from news.models import News 4 | 5 | 6 | def create(admin_client, title='Bash broken', content='Broken in [testing]', announce=False): 7 | data = { 8 | 'title': title, 9 | 'content': content, 10 | } 11 | if announce: 12 | data['send_announce'] = 'on' 13 | return admin_client.post('/news/add/', data, follow=True) 14 | 15 | 16 | def test_create_item(db, admin_client, admin_user): 17 | title = 'Bash broken' 18 | response = create(admin_client, title) 19 | assert response.status_code == 200 20 | 21 | news = News.objects.first() 22 | 23 | assert news.author == admin_user 24 | assert news.title == title 25 | 26 | 27 | def test_view(db, admin_client): 28 | create(admin_client) 29 | news = News.objects.first() 30 | 31 | response = admin_client.get(news.get_absolute_url()) 32 | assert response.status_code == 200 33 | 34 | 35 | def test_redirect_id(db, admin_client): 36 | create(admin_client) 37 | news = News.objects.first() 38 | 39 | response = admin_client.get(f'/news/{news.id}', follow=True) 40 | assert response.status_code == 200 41 | 42 | 43 | def test_send_announce(db, admin_client): 44 | title = 'New glibc' 45 | create(admin_client, title, announce=True) 46 | assert len(mail.outbox) == 1 47 | assert title in mail.outbox[0].subject 48 | 49 | 50 | def test_preview(db, admin_client): 51 | response = admin_client.post('/news/preview/', {'data': '**body**'}, follow=True) 52 | assert response.status_code == 200 53 | assert '

body

' == response.content.decode() 54 | -------------------------------------------------------------------------------- /news/tests/test_models.py: -------------------------------------------------------------------------------- 1 | def test_feed(db, client): 2 | response = client.get('/feeds/news/') 3 | assert response.status_code == 200 4 | 5 | 6 | def test_sitemap(db, client): 7 | response = client.get('/sitemap-news.xml') 8 | assert response.status_code == 200 9 | 10 | 11 | def test_news_sitemap(db, client): 12 | response = client.get('/news-sitemap.xml') 13 | assert response.status_code == 200 14 | 15 | 16 | def test_newsitem(db, client): 17 | response = client.get('/news/404', follow=True) 18 | assert response.status_code == 404 19 | -------------------------------------------------------------------------------- /news/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import permission_required 2 | from django.urls import path, re_path 3 | 4 | from main.utils import cache_user_page 5 | 6 | from .views import ( 7 | NewsCreateView, 8 | NewsDeleteView, 9 | NewsDetailView, 10 | NewsEditView, 11 | NewsListView, 12 | preview, 13 | view_redirect, 14 | ) 15 | 16 | urlpatterns = [ 17 | path('', cache_user_page(317)(NewsListView.as_view()), name='news-list'), 18 | 19 | path('preview/', preview), 20 | # old news URLs, permanent redirect view so we don't break all links 21 | re_path(r'^(?P\d+)/$', view_redirect), 22 | 23 | path('add/', 24 | permission_required('news.add_news')(NewsCreateView.as_view())), 25 | re_path(r'^(?P[-\w]+)/$', 26 | cache_user_page(317)(NewsDetailView.as_view())), 27 | re_path(r'^(?P[-\w]+)/edit/$', 28 | permission_required('news.change_news')(NewsEditView.as_view())), 29 | re_path(r'^(?P[-\w]+)/delete/$', 30 | permission_required('news.delete_news')(NewsDeleteView.as_view())), 31 | ] 32 | 33 | # vim: set ts=4 sw=4 et: 34 | -------------------------------------------------------------------------------- /packages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/packages/__init__.py -------------------------------------------------------------------------------- /packages/alpm.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import operator 3 | from ctypes.util import find_library 4 | 5 | 6 | def load_alpm(name=None): # pragma: no cover 7 | # Load the alpm library and set up some of the functions we might use 8 | if name is None: 9 | name = find_library('alpm') 10 | if name is None: 11 | # couldn't locate the correct library 12 | return None 13 | try: 14 | alpm = ctypes.cdll.LoadLibrary(name) 15 | except OSError: 16 | return None 17 | try: 18 | alpm.alpm_version.argtypes = () 19 | alpm.alpm_version.restype = ctypes.c_char_p 20 | alpm.alpm_pkg_vercmp.argtypes = (ctypes.c_char_p, ctypes.c_char_p) 21 | alpm.alpm_pkg_vercmp.restype = ctypes.c_int 22 | except AttributeError: 23 | return None 24 | 25 | return alpm 26 | 27 | 28 | ALPM = load_alpm() 29 | 30 | 31 | class AlpmAPI(object): 32 | OPERATOR_MAP = { 33 | '=': operator.eq, 34 | '==': operator.eq, 35 | '!=': operator.ne, 36 | '<': operator.lt, 37 | '<=': operator.le, 38 | '>': operator.gt, 39 | '>=': operator.ge, 40 | } 41 | 42 | def __init__(self): 43 | self.alpm = ALPM 44 | self.available = ALPM is not None 45 | 46 | def version(self): 47 | if not self.available: 48 | return None 49 | return ALPM.alpm_version() 50 | 51 | def vercmp(self, ver1, ver2): 52 | if not self.available: 53 | return None 54 | return ALPM.alpm_pkg_vercmp(str(ver1).encode(), str(ver2).encode()) 55 | 56 | def compare_versions(self, ver1, oper, ver2): 57 | func = self.OPERATOR_MAP.get(oper, None) 58 | if func is None: 59 | raise Exception("Invalid operator %s specified" % oper) 60 | if not self.available: 61 | return None 62 | res = self.vercmp(ver1, ver2) 63 | return func(res, 0) 64 | 65 | 66 | def main(): # pragma: no cover 67 | api = AlpmAPI() 68 | print((api.version())) 69 | print((api.vercmp(1, 2))) 70 | print((api.compare_versions(1, '<', 2))) 71 | 72 | 73 | if __name__ == '__main__': # pragma: no cover 74 | main() 75 | 76 | # vim: set ts=4 sw=4 et: 77 | -------------------------------------------------------------------------------- /packages/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/packages/management/__init__.py -------------------------------------------------------------------------------- /packages/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/packages/management/commands/__init__.py -------------------------------------------------------------------------------- /packages/migrations/0002_flagdenylist.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.3 on 2021-06-17 14:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('packages', '0001_squashed_0003_auto_20170524_0704'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='FlagDenylist', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('keyword', models.CharField(max_length=255)), 18 | ], 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /packages/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/packages/migrations/__init__.py -------------------------------------------------------------------------------- /packages/sql/search_indexes.postgresql_psycopg2.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pg_trgm; 2 | CREATE INDEX packages_pkgname_trgm_gist ON packages USING gist (UPPER(pkgname) gist_trgm_ops); 3 | CREATE INDEX packages_pkgdesc_trgm_gist ON packages USING gist (UPPER(pkgdesc) gist_trgm_ops); 4 | -------------------------------------------------------------------------------- /packages/sql/update.postgresql_psycopg2.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION packages_on_insert() RETURNS trigger AS $body$ 2 | BEGIN 3 | INSERT INTO packages_update 4 | (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, new_pkgver, new_pkgrel, new_epoch) 5 | VALUES (1, now(), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, NEW.pkgver, NEW.pkgrel, NEW.epoch); 6 | RETURN NULL; 7 | END; 8 | $body$ LANGUAGE plpgsql; 9 | 10 | CREATE OR REPLACE FUNCTION packages_on_update() RETURNS trigger AS $body$ 11 | BEGIN 12 | INSERT INTO packages_update 13 | (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch, new_pkgver, new_pkgrel, new_epoch) 14 | VALUES (2, now(), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch, NEW.pkgver, NEW.pkgrel, NEW.epoch); 15 | RETURN NULL; 16 | END; 17 | $body$ LANGUAGE plpgsql; 18 | 19 | CREATE OR REPLACE FUNCTION packages_on_delete() RETURNS trigger AS $body$ 20 | BEGIN 21 | INSERT INTO packages_update 22 | (action_flag, created, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch) 23 | VALUES (3, now(), OLD.arch_id, OLD.repo_id, OLD.pkgname, OLD.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch); 24 | RETURN NULL; 25 | END; 26 | $body$ LANGUAGE plpgsql; 27 | 28 | DROP TRIGGER IF EXISTS packages_insert ON packages; 29 | CREATE TRIGGER packages_insert 30 | AFTER INSERT ON packages 31 | FOR EACH ROW 32 | EXECUTE PROCEDURE packages_on_insert(); 33 | 34 | DROP TRIGGER IF EXISTS packages_update ON packages; 35 | CREATE TRIGGER packages_update 36 | AFTER UPDATE ON packages 37 | FOR EACH ROW 38 | WHEN (OLD.pkgver != NEW.pkgver OR OLD.pkgrel != NEW.pkgrel OR OLD.epoch != NEW.epoch) 39 | EXECUTE PROCEDURE packages_on_update(); 40 | 41 | DROP TRIGGER IF EXISTS packages_delete ON packages; 42 | CREATE TRIGGER packages_delete 43 | AFTER DELETE ON packages 44 | FOR EACH ROW 45 | EXECUTE PROCEDURE packages_on_delete(); 46 | -------------------------------------------------------------------------------- /packages/sql/update.sqlite3.sql: -------------------------------------------------------------------------------- 1 | DROP TRIGGER IF EXISTS packages_insert; 2 | CREATE TRIGGER packages_insert 3 | AFTER INSERT ON packages 4 | FOR EACH ROW 5 | BEGIN 6 | INSERT INTO packages_update 7 | (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, new_pkgver, new_pkgrel, new_epoch) 8 | VALUES (1, strftime('%Y-%m-%d %H:%M:%f', 'now'), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, NEW.pkgver, NEW.pkgrel, NEW.epoch); 9 | END; 10 | 11 | DROP TRIGGER IF EXISTS packages_update; 12 | CREATE TRIGGER packages_update 13 | AFTER UPDATE ON packages 14 | FOR EACH ROW 15 | WHEN (OLD.pkgver != NEW.pkgver OR OLD.pkgrel != NEW.pkgrel OR OLD.epoch != NEW.epoch) 16 | BEGIN 17 | INSERT INTO packages_update 18 | (action_flag, created, package_id, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch, new_pkgver, new_pkgrel, new_epoch) 19 | VALUES (2, strftime('%Y-%m-%d %H:%M:%f', 'now'), NEW.id, NEW.arch_id, NEW.repo_id, NEW.pkgname, NEW.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch, NEW.pkgver, NEW.pkgrel, NEW.epoch); 20 | END; 21 | 22 | DROP TRIGGER IF EXISTS packages_delete; 23 | CREATE TRIGGER packages_delete 24 | AFTER DELETE ON packages 25 | FOR EACH ROW 26 | BEGIN 27 | INSERT INTO packages_update 28 | (action_flag, created, arch_id, repo_id, pkgname, pkgbase, old_pkgver, old_pkgrel, old_epoch) 29 | VALUES (3, strftime('%Y-%m-%d %H:%M:%f', 'now'), OLD.arch_id, OLD.repo_id, OLD.pkgname, OLD.pkgbase, OLD.pkgver, OLD.pkgrel, OLD.epoch); 30 | END; 31 | -------------------------------------------------------------------------------- /packages/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/packages/templatetags/__init__.py -------------------------------------------------------------------------------- /packages/templatetags/package_extras.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import parse_qs, urlencode 2 | 3 | from django import template 4 | from django.utils.html import format_html 5 | 6 | register = template.Library() 7 | 8 | 9 | class BuildQueryStringNode(template.Node): 10 | def __init__(self, sortfield): 11 | self.sortfield = sortfield 12 | super(BuildQueryStringNode, self).__init__() 13 | 14 | def render(self, context): 15 | qs = parse_qs(context['current_query']) 16 | if 'sort' in qs and self.sortfield in qs['sort']: 17 | if self.sortfield.startswith('-'): 18 | qs['sort'] = [self.sortfield[1:]] 19 | else: 20 | qs['sort'] = ['-' + self.sortfield] 21 | else: 22 | qs['sort'] = [self.sortfield] 23 | return urlencode(qs, True).replace('&', '&') 24 | 25 | 26 | @register.tag(name='buildsortqs') 27 | def do_buildsortqs(parser, token): 28 | try: 29 | _, sortfield = token.split_contents() 30 | except ValueError: 31 | raise template.TemplateSyntaxError("%r tag requires a single argument" % token) 32 | if not (sortfield[0] == sortfield[-1] and sortfield[0] in ('"', "'")): 33 | raise template.TemplateSyntaxError("%r tag's argument should be in quotes" % token) 34 | return BuildQueryStringNode(sortfield[1:-1]) 35 | 36 | 37 | @register.simple_tag 38 | def pkg_details_link(pkg, link_title=None, honor_flagged=False): 39 | if not pkg: 40 | return link_title or '' 41 | if link_title is None: 42 | link_title = pkg.pkgname 43 | link_content = link_title 44 | if honor_flagged and pkg.flag_date: 45 | link_content = '%s' % link_title 46 | link = '%s' 47 | return format_html(link % (pkg.get_absolute_url(), pkg.pkgname, link_content)) 48 | 49 | 50 | # vim: set ts=4 sw=4 et: 51 | -------------------------------------------------------------------------------- /packages/tests/test_adopt.py: -------------------------------------------------------------------------------- 1 | from main.models import Package 2 | from packages.models import PackageRelation 3 | 4 | 5 | def request(client, pkgid, adopt=True): 6 | data = { 7 | 'pkgid': pkgid, 8 | } 9 | if adopt: 10 | data['adopt'] = 'adopt' 11 | else: 12 | data['disown'] = 'disown' 13 | return client.post('/packages/update/', data, follow=True) 14 | 15 | 16 | def test_adopt_package(developer_client, package): 17 | pkg = Package.objects.first() 18 | response = request(developer_client, pkg.id) 19 | assert response.status_code == 200 20 | assert len(PackageRelation.objects.all()) == 1 21 | 22 | response = request(developer_client, pkg.id, False) 23 | assert response.status_code == 200 24 | assert len(PackageRelation.objects.all()) == 0 25 | 26 | 27 | def test_no_permissions(client, package): 28 | pkg = Package.objects.first() 29 | 30 | response = request(client, pkg.id) 31 | assert response.status_code == 200 32 | assert len(PackageRelation.objects.all()) == 0 33 | 34 | 35 | def test_wrong_request(developer_client, package): 36 | pkg = Package.objects.first() 37 | response = developer_client.post('/packages/update/', {'pkgid': pkg.id, }, follow=True) 38 | assert response.status_code == 200 39 | assert 'Are you trying to adopt or disown' in response.content.decode() 40 | -------------------------------------------------------------------------------- /packages/tests/test_alpm.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from packages.alpm import AlpmAPI 4 | 5 | alpm = AlpmAPI() 6 | 7 | 8 | @pytest.mark.skipif(not alpm.available, reason="ALPM is unavailable") 9 | def test_version(): 10 | version = alpm.version() 11 | assert version 12 | version = version.split(b'.') 13 | # version is a 3-tuple, e.g., '7.0.2' 14 | assert len(version) == 3 15 | 16 | 17 | @pytest.mark.skipif(not alpm.available, reason="ALPM is unavailable") 18 | def test_vercmp(): 19 | assert alpm.vercmp("1.0", "1.0") == 0 20 | assert alpm.vercmp("1.1", "1.0") == 1 21 | 22 | 23 | @pytest.mark.skipif(not alpm.available, reason="ALPM is unavailable") 24 | def test_compare_versions(): 25 | assert alpm.compare_versions("1.0", "<=", "2.0") 26 | assert alpm.compare_versions("1.0", "<", "2.0") 27 | assert not alpm.compare_versions("1.0", ">=", "2.0") 28 | assert not alpm.compare_versions("1.0", ">", "2.0") 29 | assert alpm.compare_versions("1:1.0", ">", "2.0") 30 | assert not alpm.compare_versions("1.0.2", ">=", "2.1.0") 31 | 32 | assert alpm.compare_versions("1.0", "=", "1.0") 33 | assert alpm.compare_versions("1.0", "=", "1.0-1") 34 | assert not alpm.compare_versions("1.0", "!=", "1.0") 35 | 36 | 37 | def test_behavior_when_unavailable(): 38 | mock_alpm = AlpmAPI() 39 | mock_alpm.available = False 40 | 41 | assert mock_alpm.version() is None 42 | assert mock_alpm.vercmp("1.0", "1.0") is None 43 | assert mock_alpm.compare_versions("1.0", "=", "1.0") is None 44 | -------------------------------------------------------------------------------- /packages/tests/test_populate_signoffs.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | from unittest import mock 3 | 4 | from django.core.management import call_command 5 | from django.test import TransactionTestCase 6 | 7 | import packages.management.commands.populate_signoffs # noqa 8 | from main.models import Arch, Repo 9 | from packages.models import Package, SignoffSpecification 10 | 11 | 12 | class RematchDeveloperTest(TransactionTestCase): 13 | fixtures = ['main/fixtures/arches.json', 'main/fixtures/repos.json'] 14 | 15 | def setUp(self): 16 | repo = Repo.objects.get(name='Extra-Testing') 17 | arch = Arch.objects.get(name__iexact='any') 18 | now = datetime.now(tz=timezone.utc) 19 | self.package = Package.objects.create(arch=arch, repo=repo, pkgname='systemd', 20 | pkgbase='systemd', pkgver='0.1', 21 | pkgrel='1', pkgdesc='Linux kernel', 22 | compressed_size=10, installed_size=20, 23 | last_update=now, created=now) 24 | 25 | def tearDown(self): 26 | self.package.delete() 27 | 28 | def test_basic(self): 29 | with mock.patch('packages.management.commands.populate_signoffs.get_tag_info') as get_tag_info: 30 | comment = 'upgpkg: 0.1-1: rebuild' 31 | get_tag_info.return_value = {'message': f'{comment}\n', 'author': 'foo@archlinux.org'} 32 | call_command('populate_signoffs') 33 | 34 | signoff_spec = SignoffSpecification.objects.first() 35 | assert signoff_spec.comments == comment 36 | assert signoff_spec.pkgbase == self.package.pkgbase 37 | 38 | def test_invalid(self): 39 | with mock.patch('packages.management.commands.populate_signoffs.get_tag_info') as get_tag_info: 40 | get_tag_info.return_value = None 41 | call_command('populate_signoffs') 42 | 43 | assert SignoffSpecification.objects.count() == 0 44 | -------------------------------------------------------------------------------- /packages/tests/test_signoffs.py: -------------------------------------------------------------------------------- 1 | def test_signoffs(client, developer_client): 2 | response = client.get('/packages/signoffs/') 3 | assert response.status_code == 200 4 | 5 | 6 | def test_signoffs_json(client, developer_client): 7 | response = client.get('/packages/signoffs/json/') 8 | assert response.status_code == 200 9 | assert response.json()['signoff_groups'] == [] 10 | -------------------------------------------------------------------------------- /packages/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | from django.urls import path, re_path 3 | 4 | from packages import views 5 | from packages.views import display, flag, search, signoff 6 | 7 | package_patterns = [ 8 | path('', display.details), 9 | path('json/', display.details_json), 10 | path('files/', display.files), 11 | path('files/json/', display.files_json), 12 | path('flag/', flag.flag), 13 | path('flag/done/', flag.flag_confirmed, name='package-flag-confirmed'), 14 | path('unflag/', flag.unflag), 15 | path('unflag/all/', flag.unflag_all), 16 | path('signoff/', signoff.signoff_package), 17 | path('signoff/revoke/', signoff.signoff_package, {'revoke': True}), 18 | path('signoff/options/', signoff.signoff_options), 19 | path('download/', display.download), 20 | path('download.sig/', display.download, {'sig': True}), 21 | path('sonames/', display.sonames), 22 | path('sonames/json/', display.sonames_json), 23 | ] 24 | 25 | urlpatterns = [ 26 | path('flaghelp/', flag.flaghelp), 27 | path('signoffs/', signoff.signoffs, name='package-signoffs'), 28 | path('signoffs/json/', signoff.signoffs_json, name='package-signoffs-json'), 29 | path('update/', views.update), 30 | path('sonames', views.sonames), 31 | path('pkgbase-maintainer', views.pkgbase_mapping), 32 | 33 | path('', search.SearchListView.as_view(), name='packages-search'), 34 | path('search/json/', search.search_json), 35 | 36 | path('differences/', views.arch_differences, name='packages-differences'), 37 | path('stale_relations/', views.stale_relations), 38 | path('stale_relations/update/', views.stale_relations_update), 39 | 40 | re_path(r'^(?P[^ /]+)/$', display.details), 41 | re_path(r'^(?P[A-z0-9\-]+)/(?P[^ /]+)/$', display.details), 42 | # canonical package url. subviews defined above 43 | re_path(r'^(?P[A-z0-9\-]+)/(?P[A-z0-9]+)/(?P[^ /]+)/', include(package_patterns)), 44 | ] 45 | 46 | # vim: set ts=4 sw=4 et: 47 | -------------------------------------------------------------------------------- /packages/urls_groups.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, re_path 2 | 3 | from packages.views import search 4 | from packages.views.display import group_details, groups 5 | 6 | urlpatterns = [ 7 | path('', groups, name='groups-list'), 8 | path('search/json/', search.group_search_json), 9 | re_path(r'^(?P[A-z0-9]+)/$', groups), 10 | re_path(r'^(?P[A-z0-9]+)/(?P[^ /]+)/$', group_details), 11 | ] 12 | 13 | # vim: set ts=4 sw=4 et: 14 | -------------------------------------------------------------------------------- /planet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/planet/__init__.py -------------------------------------------------------------------------------- /planet/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from planet.models import Feed, FeedItem, Planet 4 | 5 | 6 | class FeedItemAdmin(admin.ModelAdmin): 7 | list_display = ('title', 'publishdate',) 8 | list_filter = ('publishdate',) 9 | search_fields = ('title',) 10 | 11 | 12 | class FeedAdmin(admin.ModelAdmin): 13 | list_display = ('title', 'website',) 14 | list_filter = ('title',) 15 | search_fields = ('title',) 16 | 17 | 18 | class PlanetAdmin(admin.ModelAdmin): 19 | list_display = ('name', 'website',) 20 | list_filter = ('name',) 21 | search_fields = ('name',) 22 | 23 | 24 | admin.site.register(Feed, FeedAdmin) 25 | admin.site.register(FeedItem, FeedItemAdmin) 26 | admin.site.register(Planet, PlanetAdmin) 27 | 28 | # vim: set ts=4 sw=4 et: 29 | -------------------------------------------------------------------------------- /planet/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/planet/management/__init__.py -------------------------------------------------------------------------------- /planet/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/planet/management/commands/__init__.py -------------------------------------------------------------------------------- /planet/migrations/0002_alter_feeditem_feed.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-06-20 19:09 2 | 3 | import django.db.models.deletion 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('planet', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='feeditem', 16 | name='feed', 17 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, 18 | related_name='items', to='planet.feed'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /planet/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/planet/migrations/__init__.py -------------------------------------------------------------------------------- /planet/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # FeedItem summary field length 4 | FEEDITEM_SUMMARY_LIMIT = 2048 5 | 6 | 7 | class Feed(models.Model): 8 | title = models.CharField(max_length=255) 9 | website = models.CharField(max_length=200, null=True, blank=True) 10 | website_rss = models.CharField(max_length=200, null=True, blank=True) 11 | 12 | def __str__(self): 13 | return self.title 14 | 15 | class Meta: 16 | db_table = 'feeds' 17 | verbose_name_plural = 'feeds' 18 | get_latest_by = 'title' 19 | ordering = ('-title',) 20 | 21 | 22 | class FeedItem(models.Model): 23 | title = models.CharField(max_length=255) 24 | summary = models.CharField(max_length=FEEDITEM_SUMMARY_LIMIT) 25 | feed = models.ForeignKey(Feed, related_name='items', 26 | on_delete=models.CASCADE, null=True) 27 | author = models.CharField(max_length=255) 28 | publishdate = models.DateTimeField("publish date", db_index=True) 29 | url = models.CharField('URL', max_length=255) 30 | 31 | def get_absolute_url(self): 32 | return self.url 33 | 34 | def __str__(self): 35 | return self.title 36 | 37 | class Meta: 38 | db_table = 'feeditems' 39 | verbose_name_plural = 'Feed Items' 40 | get_latest_by = 'publishdate' 41 | ordering = ('-publishdate',) 42 | 43 | 44 | class Planet(models.Model): 45 | ''' 46 | The planet model contains related Arch Linux planet instances. 47 | ''' 48 | 49 | name = models.CharField(max_length=255) 50 | website = models.CharField(max_length=200, null=True, blank=True) 51 | 52 | def __str__(self): 53 | return self.name 54 | 55 | class Meta: 56 | db_table = 'planets' 57 | verbose_name_plural = 'Worldwide Planets' 58 | get_latest_by = 'name' 59 | ordering = ('-name',) 60 | 61 | # vim: set ts=4 sw=4 et: 62 | -------------------------------------------------------------------------------- /planet/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/planet/tests/__init__.py -------------------------------------------------------------------------------- /planet/tests/test_views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | import feedparser 4 | 5 | from planet.models import FeedItem 6 | 7 | 8 | def test_feed(db, client): 9 | response = client.get('/feeds/planet/') 10 | assert response.status_code == 200 11 | feed = feedparser.parse(response.content) 12 | assert feed['feed']['title'] == 'Planet Arch Linux' 13 | 14 | 15 | def test_feed_item(db, client): 16 | publishdate = datetime.now(timezone.utc) 17 | FeedItem.objects.create(publishdate=publishdate, title='A title', summary='A summary', author='John Doe') 18 | 19 | response = client.get('/feeds/planet/') 20 | 21 | feed_entry = feedparser.parse(response.content)['entries'][0] 22 | assert feed_entry['published'] == publishdate.strftime('%a, %d %b %Y 00:00:00 +0000') 23 | assert feed_entry['title'] == 'A title' 24 | assert feed_entry['summary'] == 'A summary' 25 | assert feed_entry['author'] == 'John Doe' 26 | 27 | 28 | def test_planet(db, client): 29 | response = client.get('/planet/') 30 | assert response.status_code == 200 31 | -------------------------------------------------------------------------------- /planet/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.views.decorators.cache import cache_control 3 | 4 | from planet.models import Feed, FeedItem, Planet 5 | 6 | 7 | @cache_control(max_age=307) 8 | def index(request): 9 | context = { 10 | 'official_feeds': Feed.objects.all(), 11 | 'planets': Planet.objects.all(), 12 | 'feed_items': FeedItem.objects.order_by('-publishdate')[:25], 13 | } 14 | return render(request, 'planet/index.html', context) 15 | -------------------------------------------------------------------------------- /public/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/__init__.py -------------------------------------------------------------------------------- /public/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/management/__init__.py -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-black-1200dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-black-1200dpi.png -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-black-90dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-black-90dpi.png -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-dark-1200dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-dark-1200dpi.png -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-dark-90dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-dark-90dpi.png -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-light-1200dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-light-1200dpi.png -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-light-90dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-light-90dpi.png -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-only.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 16 | 17 | 19 | image/svg+xml 20 | 22 | 23 | 24 | 25 | 26 | 28 | 32 | 36 | 40 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-white-1200dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-white-1200dpi.png -------------------------------------------------------------------------------- /public/static/logos/archlinux-logo-white-90dpi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/archlinux-logo-white-90dpi.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-aqua-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-aqua-blue.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-aqua-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-aqua-white.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-aqua.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-aqua.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-blue1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-blue1.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-blue2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-blue2.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-noodle-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-noodle-blue.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-noodle-box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-noodle-box.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-noodle-cup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-noodle-cup.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-noodle-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-noodle-white.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-ribbon1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-ribbon1.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-ribbon2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-ribbon2.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-ribbon3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-ribbon3.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-ribbon4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-ribbon4.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-ribbon5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-ribbon5.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-ribbon6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-ribbon6.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-wombat-lg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-wombat-lg.png -------------------------------------------------------------------------------- /public/static/logos/legacy/arch-legacy-wombat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/public/static/logos/legacy/arch-legacy-wombat.png -------------------------------------------------------------------------------- /public/tests.py: -------------------------------------------------------------------------------- 1 | def test_index(client, arches, repos, package, groups, staff_groups): 2 | response = client.get('/') 3 | assert response.status_code == 200 4 | 5 | 6 | def test_about(client, arches, repos, package, groups, staff_groups): 7 | response = client.get('/about/') 8 | assert response.status_code == 200 9 | 10 | 11 | def test_art(client, arches, repos, package, groups, staff_groups): 12 | response = client.get('/art/') 13 | assert response.status_code == 200 14 | 15 | 16 | def test_donate(client, arches, repos, package, groups, staff_groups): 17 | response = client.get('/donate/') 18 | assert response.status_code == 200 19 | 20 | 21 | def test_download(client, arches, repos, package, groups, staff_groups): 22 | response = client.get('/download/') 23 | assert response.status_code == 200 24 | 25 | 26 | def test_master_keys(client, arches, repos, package, groups, staff_groups): 27 | response = client.get('/master-keys/') 28 | assert response.status_code == 200 29 | 30 | 31 | def test_master_keys_json(client, arches, repos, package, groups, staff_groups): 32 | response = client.get('/master-keys/json/') 33 | assert response.status_code == 200 34 | 35 | 36 | def test_feeds(client, arches, repos, package, groups, staff_groups): 37 | response = client.get('/feeds/') 38 | assert response.status_code == 200 39 | 40 | 41 | def test_people(client, arches, repos, package, groups, staff_groups): 42 | response = client.get('/people/developers/') 43 | assert response.status_code == 200 44 | 45 | 46 | def test_sitemap(client, arches, repos, package, groups, staff_groups): 47 | sitemaps = ['sitemap', 'sitemap-base'] 48 | for sitemap in sitemaps: 49 | response = client.get(f'/{sitemap}.xml') 50 | assert response.status_code == 200 51 | -------------------------------------------------------------------------------- /releng/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/releng/__init__.py -------------------------------------------------------------------------------- /releng/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Release 4 | 5 | 6 | class ReleaseAdmin(admin.ModelAdmin): 7 | list_display = ('version', 'release_date', 'kernel_version', 'available', 8 | 'created') 9 | list_filter = ('available', 'release_date') 10 | readonly_fields = ('created', 'last_modified') 11 | 12 | 13 | admin.site.register(Release, ReleaseAdmin) 14 | 15 | # vim: set ts=4 sw=4 et: 16 | -------------------------------------------------------------------------------- /releng/fixtures/release.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "fields": { 4 | "available": true, 5 | "created": "2017-06-07T19:36:49.569Z", 6 | "info": "public information", 7 | "kernel_version": "4.12", 8 | "last_modified": "2017-06-11T16:53:53.723Z", 9 | "md5_sum": "f029d6004e63464b1b26c62058c4e37e", 10 | "release_date": "2017-06-11", 11 | "wkd_email": "pierre@archlinux.de", 12 | "sha1_sum": "2c2c8ce676e891ac354cf4a8bac3824a4aae0c90", 13 | "sha256_sum": "1f2ecf3eec6013c49224445c469069b269dae47efb8e161bc6334c6611c3a1ec", 14 | "b2_sum": "8c9ffd1f8213247004e2439bdf5688db45208a0135013c100837227cc7a38d2616888cb16bb69204def929083e749c13903bad68f47f2924204d6abd58d816b3", 15 | "version": "2022.06.01" 16 | }, 17 | "model": "releng.release", 18 | "pk": 1 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /releng/migrations/0002_auto_20181216_1605.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.4 on 2018-12-16 16:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('releng', '0001_squashed_0005_auto_20180616_0947'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='release', 15 | name='last_modified', 16 | field=models.DateTimeField(editable=False), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /releng/migrations/0003_release_pgp_key.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.6 on 2020-05-22 22:14 2 | 3 | from django.db import migrations 4 | 5 | import devel.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('releng', '0002_auto_20181216_1605'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='release', 17 | name='pgp_key', 18 | field=devel.fields.PGPKeyField(blank=True, help_text='consists of 40 hex digits; use `gpg --fingerprint`', max_length=40, null=True, verbose_name='PGP key fingerprint'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /releng/migrations/0004_release_wkd_email.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.4 on 2021-06-22 04:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('releng', '0003_release_pgp_key'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='release', 15 | name='wkd_email', 16 | field=models.EmailField(blank=True, max_length=254, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /releng/migrations/0005_release_b2_sum_release_sha256_sum.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0 on 2022-01-03 08:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('releng', '0004_release_wkd_email'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='release', 15 | name='b2_sum', 16 | field=models.CharField(blank=True, max_length=128, verbose_name='B2 digest'), 17 | ), 18 | migrations.AddField( 19 | model_name='release', 20 | name='sha256_sum', 21 | field=models.CharField(blank=True, max_length=64, verbose_name='SHA256 digest'), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /releng/migrations/0006_alter_release_b2_sum.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-06-20 19:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('releng', '0005_release_b2_sum_release_sha256_sum'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='release', 15 | name='b2_sum', 16 | field=models.CharField(blank=True, max_length=128, verbose_name='BLAKE2b digest'), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /releng/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/releng/migrations/__init__.py -------------------------------------------------------------------------------- /releng/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/releng/tests/__init__.py -------------------------------------------------------------------------------- /releng/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from base64 import b64encode 2 | from datetime import datetime 3 | 4 | import pytest 5 | from bencode import bencode 6 | 7 | from releng.models import Release 8 | 9 | VERSION = '1.0' 10 | KERNEL_VERSION = '4.18' 11 | 12 | 13 | @pytest.fixture 14 | def release(db): 15 | release = Release.objects.create(release_date=datetime.now(), 16 | version=VERSION, 17 | kernel_version=KERNEL_VERSION) 18 | yield release 19 | release.delete() 20 | 21 | 22 | @pytest.fixture 23 | def torrent_data(): 24 | data = { 25 | 'comment': 'comment', 26 | 'created_by': 'Arch Linux', 27 | 'creation date': int(datetime.utcnow().timestamp()), 28 | 'info': { 29 | 'name': 'arch.iso', 30 | 'length': 1, 31 | } 32 | } 33 | return b64encode(bencode(data)).decode() 34 | -------------------------------------------------------------------------------- /releng/tests/test_models.py: -------------------------------------------------------------------------------- 1 | def test_feed(client, release): 2 | response = client.get('/feeds/releases/') 3 | assert response.status_code == 200 4 | 5 | 6 | def test_str(release): 7 | assert str(release) == release.version 8 | 9 | 10 | def test_absolute_url(release): 11 | assert release.version, release.get_absolute_url() 12 | 13 | 14 | def test_iso_url(release): 15 | url = release.iso_url() 16 | ver = release.version 17 | expected = f'iso/{ver}/archlinux-{ver}-x86_64.iso' 18 | assert url == expected 19 | 20 | 21 | def test_info_html(release): 22 | assert release.info in release.info_html() 23 | 24 | 25 | def test_dir_path(release): 26 | dir_path = f'iso/{release.version}/' 27 | assert dir_path == release.dir_path() 28 | 29 | 30 | def test_sitemap(client, release): 31 | response = client.get('/sitemap-releases.xml') 32 | assert response.status_code == 200 33 | 34 | 35 | def test_garbage_torrent_data(release): 36 | assert release.torrent() is None 37 | 38 | release.torrent_data = 'garbage' 39 | assert release.torrent() is None 40 | 41 | 42 | def test_torrent_data(release, torrent_data): 43 | release.torrent_data = torrent_data 44 | data = release.torrent() 45 | assert 'arch' in data['file_name'] 46 | 47 | 48 | def test_magnet_uri(release, torrent_data): 49 | release.torrent_data = torrent_data 50 | assert release.magnet_uri() 51 | -------------------------------------------------------------------------------- /releng/tests/test_views.py: -------------------------------------------------------------------------------- 1 | def test_release_json(client, release, torrent_data): 2 | version = release.version 3 | response = client.get('/releng/releases/json/') 4 | assert response.status_code == 200 5 | 6 | data = response.json() 7 | assert data['version'] == 1 8 | release_data = data['releases'][0] 9 | assert release_data['version'] == version 10 | 11 | # Test with torrent data 12 | release.torrent_data = torrent_data 13 | release.save() 14 | response = client.get('/releng/releases/json/') 15 | assert response.status_code == 200 16 | 17 | 18 | def test_json(db, client): 19 | response = client.get('/releng/releases/json/') 20 | assert response.status_code == 200 21 | 22 | data = response.json() 23 | assert data['releases'] == [] 24 | 25 | 26 | def test_netboot_page(db, client): 27 | response = client.get('/releng/netboot/') 28 | assert response.status_code == 200 29 | 30 | 31 | def test_netboot_config(db, client): 32 | response = client.get('/releng/netboot/archlinux.ipxe') 33 | assert response.status_code == 200 34 | 35 | 36 | def test_release_torrent(client, release, torrent_data): 37 | response = client.get(f'/releng/releases/{release.version}/torrent/') 38 | assert response.status_code == 404 39 | 40 | release.torrent_data = torrent_data 41 | release.save() 42 | response = client.get(f'/releng/releases/{release.version}/torrent/') 43 | assert response.status_code == 200 44 | 45 | 46 | def test_release_details(client, release): 47 | response = client.get(f'/releng/releases/{release.version}/') 48 | assert response.status_code == 200 49 | assert release.version in response.content.decode() 50 | -------------------------------------------------------------------------------- /releng/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include 2 | from django.urls import path, re_path 3 | from django.views.decorators.cache import cache_page 4 | 5 | from releng import views 6 | 7 | from .views import ReleaseDetailView, ReleaseListView 8 | 9 | releases_patterns = [ 10 | path('', ReleaseListView.as_view(), name='releng-release-list'), 11 | path('json/', views.releases_json, name='releng-release-list-json'), 12 | re_path(r'^(?P[-.\w]+)/$', cache_page(311)(ReleaseDetailView.as_view()), 13 | name='releng-release-detail'), 14 | re_path(r'^(?P[-.\w]+)/torrent/$', cache_page(311)(views.release_torrent), 15 | name='releng-release-torrent'), 16 | ] 17 | 18 | netboot_patterns = [ 19 | path('archlinux.ipxe', views.netboot_config, name='releng-netboot-config'), 20 | path('', views.netboot_info, name='releng-netboot-info') 21 | ] 22 | 23 | urlpatterns = [ 24 | path('releases/', include(releases_patterns)), 25 | path('netboot/', include(netboot_patterns)), 26 | ] 27 | 28 | # vim: set ts=4 sw=4 et: 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e git+https://github.com/fredj/cssmin.git@master#egg=cssmin 2 | Django==5.1.9 3 | IPy==1.1 4 | Markdown==3.3.7 5 | bencode.py==4.0.0 6 | django-countries==7.6.1 7 | django-extensions==4.1 8 | jsmin==3.0.1 9 | pgpdump==1.5 10 | parse==1.20.2 11 | sqlparse==0.5.0 12 | django-csp==4.0 13 | ptpython==2.0.4 14 | feedparser==6.0.11 15 | bleach==6.0.0 16 | requests==2.32.3 17 | xtarfile==0.2.1 18 | zstandard==0.23.0 19 | django-prometheus==2.3.1 20 | -------------------------------------------------------------------------------- /requirements_prod.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | pyinotify==0.9.6 3 | pymemcache==3.5.0 4 | pyasyncore==1.0.4 5 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | pytest 3 | pytest-cov 4 | pytest-django 5 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 118 2 | 3 | [lint] 4 | select = [ 5 | "A", # flake8-builtins 6 | "B", # flake8-bugbear 7 | "C4", # flake8-comprehensions 8 | "DJ", # flake8-django 9 | "E", # pycodestyle 10 | "F", # pyflakes 11 | "G", # flake8-logging-format 12 | "I", # isort 13 | "ICN", # flake8-import-conventions 14 | "ISC", # flake8-implicit-str-concat 15 | "PIE", # flake8-pie 16 | "PLE", # pylint errors 17 | "RSE", # flake8-raise 18 | "RUF", # ruff rules 19 | "T10", # flake8-debugger 20 | "TCH", # flake8-type-checking 21 | "UP032", # f-string 22 | "W", # warnings (mostly whitespace) 23 | "YTT", # flake8-2020 24 | ] 25 | 26 | ignore = [ 27 | "E731", # Do not assign a `lambda` expression, use a `def` 28 | "B904", # Within an `except` clause, raise exceptions with `raise ... from err` 29 | "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` 30 | # TODO: add these one by one 31 | "DJ001", # Avoid using `null=True` on string-based fields such as `CharField` 32 | "DJ006", # Do not use `exclude` with `ModelForm`, use `fields` instead 33 | "DJ008", # Model does not define `__str__` method 34 | "DJ012", # Order of model's inner classes, methods, and fields does not follow the Django Style Guide: `Meta` class should come before `get_absolute_url` 35 | ] 36 | 37 | exclude = [ 38 | "*/migrations/*.py", # Ignore Django migrations 39 | "src/cssmin/*" # cssmin, not our code 40 | ] 41 | -------------------------------------------------------------------------------- /sitestatic/archnavbar/archlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/archnavbar/archlogo.png -------------------------------------------------------------------------------- /sitestatic/archnavbar/archnavbar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * ARCH GLOBAL NAVBAR 3 | * We're forcing all generic selectors with !important 4 | * to help prevent other stylesheets from interfering. 5 | */ 6 | 7 | /* container for the entire bar */ 8 | #archnavbar { min-height: 40px !important; padding: 10px 15px !important; background: #333 !important; border-bottom: 5px #08c solid !important; } 9 | #archnavbarlogo { background: url('archlogo.png') no-repeat !important; } 10 | 11 | /* move the heading/paragraph text offscreen */ 12 | #archnavbarlogo p { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } 13 | #archnavbarlogo h1 { margin: 0 !important; padding: 0 !important; text-indent: -9999px !important; } 14 | 15 | /* make the link the same size as the logo */ 16 | #archnavbarlogo a { display: block !important; height: 40px !important; width: 190px !important; } 17 | 18 | /* display the list inline, float it to the right and style it */ 19 | #archnavbar ul { display: block !important; list-style: none !important; margin: 0 !important; padding: 0 !important; font-size: 0px !important; text-align: right !important; } 20 | #archnavbar ul li { display: inline-block !important; font-size: 14px !important; font-family: sans-serif !important; line-height: 14px !important; padding: 14px 15px 0px !important; } 21 | 22 | /* style the links */ 23 | #archnavbar ul#archnavbarlist li a { color: #999; font-weight: bold !important; text-decoration: none !important; } 24 | #archnavbar ul li a:hover { color: white !important; text-decoration: underline !important; } 25 | 26 | -------------------------------------------------------------------------------- /sitestatic/click_and_pledge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/click_and_pledge.png -------------------------------------------------------------------------------- /sitestatic/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/download.png -------------------------------------------------------------------------------- /sitestatic/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/favicon.png -------------------------------------------------------------------------------- /sitestatic/flaghelp.css: -------------------------------------------------------------------------------- 1 | body { background: #f6f9fc; color: #222; font-family: sans-serif; } 2 | a:link { text-decoration: none; color: #07b; } 3 | a:visited { color: #666; } 4 | a:hover { text-decoration: underline; color: #666; } 5 | -------------------------------------------------------------------------------- /sitestatic/flags/fam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/flags/fam.png -------------------------------------------------------------------------------- /sitestatic/hetzner_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/hetzner_logo.png -------------------------------------------------------------------------------- /sitestatic/icons8_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/icons8_logo.png -------------------------------------------------------------------------------- /sitestatic/logos/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/logos/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /sitestatic/logos/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/logos/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /sitestatic/logos/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/logos/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /sitestatic/logos/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/logos/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /sitestatic/logos/icon-transparent-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/logos/icon-transparent-64x64.png -------------------------------------------------------------------------------- /sitestatic/magnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/magnet.png -------------------------------------------------------------------------------- /sitestatic/netboot/ipxe-arch.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/netboot/ipxe-arch.efi -------------------------------------------------------------------------------- /sitestatic/netboot/ipxe-arch.efi.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/netboot/ipxe-arch.efi.sig -------------------------------------------------------------------------------- /sitestatic/netboot/ipxe-arch.lkrn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/netboot/ipxe-arch.lkrn -------------------------------------------------------------------------------- /sitestatic/netboot/ipxe-arch.lkrn.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/netboot/ipxe-arch.lkrn.sig -------------------------------------------------------------------------------- /sitestatic/netboot/ipxe-arch.pxe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/netboot/ipxe-arch.pxe -------------------------------------------------------------------------------- /sitestatic/netboot/ipxe-arch.pxe.sig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/netboot/ipxe-arch.pxe.sig -------------------------------------------------------------------------------- /sitestatic/nitrokey_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/nitrokey_logo.png -------------------------------------------------------------------------------- /sitestatic/rss.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sitestatic/shells_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/shells_logo.png -------------------------------------------------------------------------------- /sitestatic/silhouette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/sitestatic/silhouette.png -------------------------------------------------------------------------------- /templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

403 - Access Forbidden

6 |

Sorry, the page you've requested is not available.

7 |
8 | {% endblock %} 9 | 10 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - Page Not Found{% endblock %} 3 | 4 | {% block content %} 5 |
6 |

404 - Page Not Found

7 |

Sorry, the page you've requested does not exist.

8 |
9 | {% endblock %} 10 | 11 | -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

500 - Internal Server Error

6 |

Something has gone horribly wrong. Back away slowly.

7 |
8 | {% endblock %} 9 | 10 | -------------------------------------------------------------------------------- /templates/devel/admin_log.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/base_site.html" %} 2 | {% load i18n static %} 3 | 4 | {% block extrastyle %}{{ block.super }}{% endblock %} 5 | 6 | {% block breadcrumbs %}{% endblock %} 7 | 8 | {% block content %} 9 |
10 |
11 | {% load log %} 12 | {% if log_user %} 13 | {% get_admin_log 100 as admin_log for_user log_user %} 14 | {% else %} 15 | {% get_admin_log 100 as admin_log %} 16 | {% endif %} 17 | {% if not admin_log %} 18 |

{% trans 'None available' %}

19 | {% else %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {% for entry in admin_log %} 32 | 33 | 34 | {% if log_user %} 35 | 36 | {% else %} 37 | 38 | {% endif %} 39 | 46 | 54 | 55 | 56 | {% endfor %} 57 | 58 |
{% trans 'Date/time' %}{% trans 'User' %}TypeObject{% trans 'Action' %}
{{ entry.action_time|date:"Y-m-d H:i" }}{{ entry.user.username }}{% if entry.user.get_full_name %} ({{ entry.user.get_full_name }}){% endif %}{{ entry.user.username }}{% if entry.user.get_full_name %} ({{ entry.user.get_full_name }}){% endif %} 40 | {% if entry.content_type %} 41 | {% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %} 42 | {% else %} 43 | {% trans 'Unknown content' %} 44 | {% endif %} 45 | 47 | 48 | {% if entry.is_deletion %} 49 | {{ entry.object_repr }} 50 | {% else %} 51 | {{ entry.object_repr }} 52 | {% endif %} 53 | {{ entry.change_message }}
59 | {% endif %} 60 |
61 |
62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /templates/devel/email_reproduciblebuilds.txt: -------------------------------------------------------------------------------- 1 | The following packages have become non reproducible: 2 | {% for pkg in pkgs %} 3 | * {{ pkg }} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /templates/devel/new_account.txt: -------------------------------------------------------------------------------- 1 | You can now log into https://{{ site.domain }}/login/ with these login details: 2 | Username: {{ user.username }} 3 | Password: {{ password }} 4 | 5 | Please update your profile once logged in and change your password. 6 | -------------------------------------------------------------------------------- /templates/devel/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Arch Linux - Edit Profile{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 |

Developer Profile

10 | 11 |
{% csrf_token %} 12 |

Note: This is the public information shown on the developer 13 | and/or package maintainer profiles page, so please be appropriate with the information 14 | you provide here.

15 |
16 |

17 | {{ user.username }}

18 | {{ form.as_p }} 19 |
20 |
21 | {{ profile_form.as_p }} 22 |
23 |

24 |
25 | 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/devel/tier0_mirror.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Arch Linux - Tier0 Mirror{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 |

Tier 0 Mirror usage information

10 |

Arch Linux Tier 0 mirror on repos.archlinux.org which can be used if to obtain the absolute latest packages. The mirror is protected with an HTTP Basic Auth password unique per Staff member.

11 | {% if mirror_url %} 12 | Server = {{ mirror_url }} 13 | 14 |
{% csrf_token %} 15 |

16 |
17 | {% else %} 18 |
{% csrf_token %} 19 |

20 |
21 | {% endif %} 22 |
23 | {% endblock %} 24 | 25 | {% block script_block %} 26 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /templates/general_form.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - {{title}}{% endblock %} 3 | 4 | {% block content %} 5 |
6 | 7 |

{{title}}

8 | 9 | {{description}} 10 | {{form.non_field_errors}} 11 | 12 |
{% csrf_token %} 13 |
14 | {% for field in form %} 15 | {{field.errors}} 16 |

17 | {% if field.help_text %}
{{field.help_text}}{% endif %} 18 | {{field}} 19 | {% if field.field.required %}*{% endif %} 20 |

21 | {% endfor %} 22 |
23 |

24 |
25 | 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/mirrors/error_table.html: -------------------------------------------------------------------------------- 1 | {% load flags %} 2 | {% load mirror_status %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for log in error_logs %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% endfor %} 26 | 27 |
Mirror URLProtocolCountryError MessageLast OccurredOccurrences (last {{ cutoff|hours }})
{{ log.url.url }}{{ log.url.protocol.protocol }}{% country_flag log.url.country %}{{ log.url.country.name }}{{ log.error|linebreaksbr }}{{ log.last_occurred|date:'Y-m-d H:i' }}{{ log.error_count }}details
28 | -------------------------------------------------------------------------------- /templates/mirrors/mirror_details_urls.html: -------------------------------------------------------------------------------- 1 | {% load flags %} 2 | {% load mirror_status %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for m_url in urls %} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% endfor %} 38 | 39 |
Mirror URLProtocolCountryIPv4IPv6Last SyncCompletion %μ Delay (hh:mm)μ Duration (s)σ Duration (s)ScoreDetails
{% if m_url.protocol.is_download %}{{ m_url.url }}{% else %}{{ m_url.url }}{% endif %}{{ m_url.protocol }}{% country_flag m_url.country %}{{ m_url.country.name }}{{ m_url.has_ipv4|yesno|capfirst }}{{ m_url.has_ipv6|yesno|capfirst }}{{ m_url.last_sync|date:'Y-m-d H:i'|default:'unknown' }}{{ m_url.completion_pct|percentage:1 }}{{ m_url.delay|duration|default:'unknown' }}{{ m_url.duration_avg|floatformat:2 }}{{ m_url.duration_stddev|floatformat:2 }}{{ m_url.score|floatformat:1|default:'∞' }}Details
40 | -------------------------------------------------------------------------------- /templates/mirrors/mirrorlist.txt: -------------------------------------------------------------------------------- 1 | {% comment %} 2 | Yes, ugly templates are ugly, but in order to keep line breaks where we want 3 | them, sacrifices have to be made. If editing this template, it is easiest to 4 | forget about where line breaks are happening until you are done getting the 5 | content right, and then go back later to fix it all up. 6 | {% endcomment %}{% autoescape off %}## 7 | ## Arch Linux repository mirrorlist 8 | ## Generated on {% now "Y-m-d" %} 9 | ##{% for mirror_url in mirror_urls %}{% ifchanged %} 10 | 11 | ## {{ mirror_url.country.name|default:'Worldwide' }}{% endifchanged %} 12 | #Server = {{ mirror_url.url}}$repo/os/$arch{% endfor %} 13 | {% endautoescape %} 14 | -------------------------------------------------------------------------------- /templates/mirrors/mirrorlist_generate.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load package_extras %} 3 | {% block title %}Arch Linux - Pacman Mirrorlist Generator{% endblock %} 4 | 5 | {% block content %} 6 |
7 | 8 |

Pacman Mirrorlist Generator

9 | 10 |

This page generates the most up-to-date mirrorlist possible for Arch 11 | Linux. The data used here comes straight from the developers' internal 12 | mirror database used to track mirror availability and tiering. There are 13 | two main options: get a mirrorlist with every available mirror, or get a 14 | mirrorlist tailored to your geography.

15 | 16 |

Mirrorlist with all available mirrors

17 | 18 |

An up-to-date mirrorlist is available containing all currently active 19 | mirrors, optionally filtering by protocol. These URLs requires no GET or 20 | POST parameters so they can be fetched from the command line if desired.

21 | 22 |

27 | 28 |

Customized by country mirrorlist

29 | 30 |

The following form can generate a custom up-to-date 31 | pacman mirrorlist based on geography and 33 | desired protocol(s). Simply replace the contents of 34 | /etc/pacman.d/mirrorlist with your generated list. 35 | Additionally, the mirror status data can be incorporated into the generated 36 | mirror list and used to only list up to date mirrors.

37 | 38 |
39 | {{ mirrorlist_form.as_div }} 40 |

41 |
42 |
43 | {% endblock %} 44 | 45 | -------------------------------------------------------------------------------- /templates/mirrors/mirrorlist_status.txt: -------------------------------------------------------------------------------- 1 | {% load mirror_status %}{% comment %} 2 | Yes, ugly templates are ugly, but in order to keep line breaks where we want 3 | them, sacrifices have to be made. If editing this template, it is easiest to 4 | forget about where line breaks are happening until you are done getting the 5 | content right, and then go back later to fix it all up. 6 | {% endcomment %}{% autoescape off %}## 7 | ## Arch Linux repository mirrorlist 8 | ## Filtered by mirror score from mirror status page 9 | ## Generated on {% now "Y-m-d" %} 10 | ## 11 | {% for mirror_url in mirror_urls %} 12 | ## {{ mirror_url.country.name|default:'Worldwide' }} 13 | #Server = {{ mirror_url.url}}$repo/os/$arch{% endfor %} 14 | {% endautoescape %} 15 | -------------------------------------------------------------------------------- /templates/mirrors/mirrors.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load flags %} 4 | 5 | {% block title %}Arch Linux - Mirror Overview{% endblock %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |

Mirror Overview

12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% if user.is_authenticated %} 21 | 22 | 23 | 24 | 25 | {% endif %} 26 | 27 | 28 | 29 | {% for mirror in mirror_list %} 30 | 31 | 33 | 34 | 35 | 36 | 37 | {% if user.is_authenticated %} 38 | 39 | 40 | 41 | 42 | {% endif %} 43 | 44 | {% endfor %} 45 | 46 |
ServerCountryTierISOsProtocolsPublicActiveAdmin EmailNotes
{{ mirror.name }}{% if mirror.country %}{% country_flag mirror.country %}{{ mirror.country.name }}{% else %}Various{% endif %}{{ mirror.get_tier_display }}{{ mirror.isos|yesno|capfirst }}{{ mirror.protocols|join:", " }}{{ mirror.public|yesno|capfirst }}{{ mirror.active|yesno|capfirst }}{{ mirror.admin_email }}{{ mirror.notes|linebreaks }}
47 |
48 | {% load cdn %}{% jquery %}{% jquery_tablesorter %} 49 | 50 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /templates/mirrors/status_table.html: -------------------------------------------------------------------------------- 1 | {% load flags %} 2 | {% load mirror_status %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for m_url in urls %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
Mirror URLProtocolCountryCompletion %μ Delay (hh:mm)μ Duration (s)σ Duration (s)Mirror Score
{{ m_url.url }}{{ m_url.protocol }}{% country_flag m_url.country %}{{ m_url.country.name }}{{ m_url.completion_pct|percentage:1 }}{{ m_url.delay|duration }}{{ m_url.duration_avg|floatformat:2 }}{{ m_url.duration_stddev|floatformat:2 }}{{ m_url.score|floatformat:1|default:'∞' }}details
31 | -------------------------------------------------------------------------------- /templates/mirrors/url_details.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load mirror_status %} 4 | {% load flags %} 5 | 6 | {% block title %}Arch Linux - {{ url.url }} - URL Details{% endblock %} 7 | 8 | {% block head %}{% endblock %} 9 | 10 | {% block content %} 11 |
12 |

URL Details: {{ url.url }}

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% if user.is_authenticated %} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% endif %} 57 |
URL:{% if url.protocol.is_download %}{{ url.url }}{% else %}{{ url.url }}{% endif %}
Mirror:{{ url.mirror.name }}
Protocol:{{ url.protocol }}
Country:{% country_flag url.country %}{{ url.country.name }}
IPv4:{{ url.has_ipv4|yesno|capfirst }}
IPv6:{{ url.has_ipv6|yesno|capfirst }}
Active:{{ url.active|yesno|capfirst }}
Created:{{ url.created }}
First Check:{{ url.logs.earliest.check_time }}
Last Check:{{ url.logs.latest.check_time }}
58 | 59 |

Check Logs

60 | {% include "mirrors/url_details_logs.html" %} 61 |
62 | {% endblock %} 63 | 64 | {% block script_block %} 65 | {% load cdn %}{% jquery %}{% jquery_tablesorter %} 66 | 67 | 74 | {% endblock %} 75 | -------------------------------------------------------------------------------- /templates/mirrors/url_details_logs.html: -------------------------------------------------------------------------------- 1 | {% load flags %} 2 | {% load mirror_status %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {% for log in logs %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {% endfor %} 28 | 29 |
Check TimeCheck LocationCheck IPLast SyncDelay (hh:mm)Duration (s)Success?Error Message
{{ log.check_time|date:'Y-m-d H:i' }}{% if log.location %}{% country_flag log.location.country %}{{ log.location.country.name }}{% else %}Unknown{% endif %}{% if log.location %}{{ log.location.source_ip }}{% else %}Unknown{% endif %}{{ log.last_sync|date:'Y-m-d H:i' }}{{ log.delay|duration }}{{ log.duration|floatformat:2 }}{{ log.is_success|yesno|capfirst }}{{ log.error|linebreaksbr }}
30 | -------------------------------------------------------------------------------- /templates/news/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - Delete News{% endblock %} 3 | 4 | {% block content %} 5 |
6 | 7 |

News: Delete Entry Confirmation

8 | 9 |

You are about to delete the following news item:

10 | 11 |
12 | {{news}} 13 |
14 | 15 |

Are you sure?

16 | 17 |
{% csrf_token %} 18 |

20 |
21 | 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/news/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Arch Linux - News{% endblock %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 | 12 |

Arch Linux News Archives

13 | 14 | {% if perms.news.add_news %} 15 | 18 | {% endif %} 19 | 20 | {% include "news/paginator.html" %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% if perms.news.change_news %} 29 | 30 | {% endif %} 31 | 32 | 33 | 34 | {% for item in news_list %} 35 | 36 | 37 | 39 | 40 | {% if perms.news.change_news %} 41 | 49 | {% endif %} 50 | 51 | {% endfor %} 52 | 53 |
PublishedTitleAuthor
{{ item.postdate|date:"Y-m-d" }}{{ item.title }}{{ item.author.get_full_name }} 42 | Edit 44 | {% endif %} 45 | {% if perms.news.delete_news %} 46 |   Delete 48 |
54 | 55 | {% include "news/paginator.html" %} 56 |
57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /templates/news/news_email_notification.txt: -------------------------------------------------------------------------------- 1 | {{ news.content }} 2 | 3 | URL: https://archlinux.org{{ news.get_absolute_url }} 4 | -------------------------------------------------------------------------------- /templates/news/paginator.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 | 22 | {% endif %} 23 | -------------------------------------------------------------------------------- /templates/news/view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - News: {{ news.title }}{% endblock %} 3 | 4 | {% block content %} 5 |
6 |

{{ news.title }}

7 | 8 | 9 | 10 | 11 | 12 | 15 |
16 | 17 |
18 | 19 | {% if perms.news.change_news %} 20 | 26 | {% endif %} 27 | 28 | 29 | 30 |
{{ news.html }}
31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /templates/packages/approved.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %} The {{ pkg.repo.name }} package {{ pkg.pkgname }} {{ pkg.full_version }} has received the required number of signoffs. 2 | 3 | The package was signed off by the following users:{% for s in signoffs %} 4 | - {{ s.user.username }}{% endfor %}{% endautoescape %} 5 | -------------------------------------------------------------------------------- /templates/packages/details_depend.html: -------------------------------------------------------------------------------- 1 | {% load details_link %} 2 | {% for depend in deps %} 3 |
  • {% if depend.pkg == None %} 4 | {% if depend.providers %}{{ depend.dep.name }}{{ depend.dep.comparison|default:""}}{{ depend.dep.version|default:"" }} ({% for pkg in depend.providers %}{% details_link pkg %}{% if not forloop.last %}, {% endif %}{% endfor %}) 5 | {% else %}{{ depend.dep.name }}{{ depend.dep.comparison|default:""}}{{ depend.dep.version|default:"" }} (virtual){% endif %} 6 | {% else %} 7 | {% details_link depend.pkg %}{{ depend.dep.comparison|default:""}}{{ depend.dep.version|default:"" }} 8 | {% if depend.pkg.repo.testing %} (testing){% endif %} 9 | {% if depend.pkg.repo.staging %} (staging){% endif %} 10 | {% endif %} 11 | {% if depend.dep.deptype == 'O' %} (optional){% endif %} 12 | {% if depend.dep.deptype == 'M' %} (make){% endif %} 13 | {% if depend.dep.deptype == 'C' %} (check){% endif %} 14 | {% if depend.dep.description %} - {{ depend.dep.description }}{% endif %}
  • 15 | {% endfor %} 16 | -------------------------------------------------------------------------------- /templates/packages/details_relatedto.html: -------------------------------------------------------------------------------- 1 | {% load details_link %} 2 | {% for related in all_related %}{% with best_satisfier=related.get_best_satisfier %} 3 | {% if best_satisfier == None %}{{ related.name }}{% else %}{% spaceless %}{% details_link best_satisfier %}{% endspaceless %}{% endif %}{{ related.comparison|default:'' }}{{ related.version|default:'' }}{% if not forloop.last %}, {% endif %} 4 | {% endwith %}{% endfor %} 5 | -------------------------------------------------------------------------------- /templates/packages/details_requiredby.html: -------------------------------------------------------------------------------- 1 | {% load details_link %} 2 | {% for req in rqdby %} 3 |
  • {% details_link req.pkg %} 4 | {% if req.name != pkg.pkgname %} (requires {{ req.name }}) 5 | {% endif %}{% if req.pkg.repo.testing %} (testing) 6 | {% endif %}{% if req.pkg.repo.staging %} (staging) 7 | {% endif %}{% if req.deptype == 'O' %} (optional) 8 | {% endif %}{% if req.deptype == 'M' %} (make) 9 | {% endif %}{% if req.deptype == 'C' %} (check) 10 | {% endif %}
  • 11 | {% endfor %} 12 | -------------------------------------------------------------------------------- /templates/packages/differences.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load package_extras %} 4 | 5 | {% block title %}Arch Linux - Package Differences Reports{% endblock %} 6 | {% block navbarclass %}anb-packages{% endblock %} 7 | 8 | {% block content %} 9 |
    10 |

    Multilib Differences to Main Packages

    11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for pkg1, pkg2 in multilib_differences %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% endfor %} 36 | 37 |
    Multilib NameMultilib Versionx86_64 Versionx86_64 Namex86_64 RepoMultilib Last Updatedx86_64 Last Updated
    {% pkg_details_link pkg1 %}{{ pkg1.full_version }}{{ pkg2.full_version }}{% pkg_details_link pkg2 %}{{ pkg2.repo }}{{ pkg1.last_update|date:"Y-m-d" }}{{ pkg2.last_update|date:"Y-m-d" }}
    38 | 39 |
    40 | {% endblock %} 41 | 42 | {% block script_block %} 43 | {% load cdn %}{% jquery %}{% jquery_tablesorter %} 44 | 45 | 50 | {% endblock %} 51 | -------------------------------------------------------------------------------- /templates/packages/files.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - {{ pkg.pkgname }} {{ pkg.full_version }} ({{ pkg.arch.name }}) - File List{% endblock %} 3 | {% block navbarclass %}anb-packages{% endblock %} 4 | 5 | {% block content %} 6 |
    7 | 8 |

    {{ pkg.pkgname }} {{ pkg.full_version }} File List

    9 |

    Package has {{ files_count }} file{{ files_count|pluralize }} and {{ dir_count }} director{{ dir_count|pluralize:"y,ies" }}.

    10 |

    Back to Package

    11 |
    12 | {% include "packages/files_list.html" %} 13 |
    14 | 15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/packages/files_list.html: -------------------------------------------------------------------------------- 1 | {% if pkg.last_update > pkg.files_last_update %} 2 |

    Note: This file list was generated from a previous version 3 | of the package; it may be out of date.

    4 | {% endif %} 5 | {% if pkg.files_last_update %} 6 | {% if files|length %} 7 |
      8 | {% for file in files %} 9 |
    • {{ file.directory }}{{ file.filename|default:'' }}
    • {% endfor %} 10 |
    11 | {% else %} 12 |

    Package has no files.

    13 | {% endif %} 14 | {% else %} 15 |

    No file list available.

    16 | {% endif %} 17 | -------------------------------------------------------------------------------- /templates/packages/flag_confirmed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load package_extras %} 3 | 4 | {% block title %}Arch Linux - Package Flagged - {{ package.pkgname }} {{ package.full_version }} ({{ package.arch.name }}){% endblock %} 5 | {% block head %}{% endblock %} 6 | {% block navbarclass %}anb-packages{% endblock %} 7 | 8 | {% block content %} 9 |
    10 |

    Package Flagged - {{ package.pkgname }}

    11 | 12 |

    Thank you, the maintainers have been notified the following 13 | {{ packages|length }} package{{ packages|pluralize }} are out-of-date:

    14 |
      15 | {% for pkg in packages %} 16 |
    • {% pkg_details_link pkg %} {{ pkg.full_version }} [{{ pkg.repo.name|lower }}] ({{ pkg.arch.name }})
    • 17 | {% endfor %} 18 |
    19 | 20 |

    You can return to the package details page for {% pkg_details_link package %}.

    21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/packages/flagged.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load package_extras %} 3 | 4 | {% block title %}Arch Linux - Flag Package - {{ pkg.pkgname }} {{ pkg.full_version }} ({{ pkg.arch.name }}){% endblock %} 5 | {% block head %}{% endblock %} 6 | {% block navbarclass %}anb-packages{% endblock %} 7 | 8 | {% block content %} 9 |
    10 |

    Package {{ pkg.pkgname }} {{ pkg.full_version }} ({{ pkg.arch.name }}) already flagged

    11 | 12 |

    {{pkg.pkgname}} has already been flagged out-of-date.

    13 | 14 |

    You can return to the package details page for {% pkg_details_link pkg %}.

    15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /templates/packages/flaghelp.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | Flagging Packages 5 | 6 | 7 | 8 | 9 | 10 | 11 |

    Flagging Packages

    12 |

    If you notice that a package is out-of-date (i.e., there is a newer 13 | stable release available), then please notify us by 14 | using the Flag button in the Package Details 15 | screen. This will notify the maintainer(s) responsible for that 16 | package so they can update it. If the package is unmaintained, the 17 | notification will be sent to a developer mailing list.

    18 | 19 |

    The message box portion of the flag utility is meant 20 | for short messages only. If you need more than 200 characters for your 21 | message, then file a bug report, email the maintainer directly, or send 22 | an email to the arch-general mailing list 24 | with your additional text.

    25 | 26 |

    Note: Please do not use this facility if the 27 | package is broken! File an issue on the package's GitLab repository 29 | instead.

    30 | 31 | 32 | -------------------------------------------------------------------------------- /templates/packages/groups.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Arch Linux - Package Groups{% if arch %} - {{ arch }}{% endif %}{% endblock %} 5 | {% block navbarclass %}anb-packages{% endblock %} 6 | 7 | {% block content %} 8 |
    9 |

    Package Groups Overview{% if arch %} - {{ arch }}{% endif %}

    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for grp in groups %} 21 | 22 | 23 | 25 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
    ArchGroup NamePackage CountLast Updated
    {{ grp.arch }}{{ grp.name }}{{ grp.count }}{{ grp.last_update|date:"Y-m-d" }}
    31 |
    32 | {% endblock %} 33 | 34 | {% block script_block %} 35 | {% load cdn %}{% jquery %}{% jquery_tablesorter %} 36 | 37 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /templates/packages/opensearch.xml: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | Arch Packages 4 | Arch Linux Package Repository Search 5 | Search the Arch Linux package repositories by keyword in package names and descriptions. 6 | linux archlinux package software 7 | {{ domain }}{% static "favicon.png" %} 8 | {{ domain }}{% static "logos/icon-transparent-64x64.png" %} 9 | en-us 10 | UTF-8 11 | UTF-8 12 | 13 | 14 | 16 | 18 | 19 | -------------------------------------------------------------------------------- /templates/packages/outofdate.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %}{{ email }} wants to notify you that the following packages may be out-of-date: 2 | 3 | {% for p in packages %} 4 | * {{ p.pkgname }} {{ p.full_version }} [{{ p.repo.name|lower }}] ({{ p.arch.name }}): {{ p.get_full_url }}{% endfor %} 5 | 6 | {% if message %} 7 | The user provided the following additional text: 8 | 9 | {{ message }} 10 | {% endif %}{% endautoescape %} 11 | -------------------------------------------------------------------------------- /templates/packages/packages_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load package_extras %} 4 | 5 | {% block title %}Arch Linux - {{ name }} ({{ arch.name }}) - {{ list_title }}{% endblock %} 6 | {% block navbarclass %}anb-packages{% endblock %} 7 | 8 | {% block content %} 9 |
    10 |

    {{ list_title }} - {{ name }} ({{ arch.name }})

    11 |

    {{ packages|length }} package{{ packages|pluralize }} found.

    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for pkg in packages %} 26 | 27 | 28 | 29 | 30 | {% if pkg.flag_date %} 31 | 32 | {% else %} 33 | 34 | {% endif %} 35 | 36 | 37 | 38 | 39 | {% endfor %} 40 | 41 |
    ArchRepoNameVersionDescriptionLast UpdatedFlag Date
    {{ pkg.arch.name }}{{ pkg.repo.name|capfirst }}{% pkg_details_link pkg %}{{ pkg.full_version }}{{ pkg.full_version }}{{ pkg.pkgdesc }}{{ pkg.last_update|date:"Y-m-d" }}{{ pkg.flag_date|date:"Y-m-d" }}
    42 |
    43 | {% endblock %} 44 | 45 | {% block script_block %} 46 | {% load cdn %}{% jquery %}{% jquery_tablesorter %} 47 | 48 | 53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /templates/packages/removed.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load package_extras %} 3 | 4 | {% block title %}Arch Linux - Not Available - {{ name }} {{ version }} ({{ arch.name }}){% endblock %} 5 | {% block navbarclass %}anb-packages{% endblock %} 6 | 7 | {% block content %} 8 |
    9 |

    {{ name }} {{ version }} is no longer available

    10 | 11 |

    {{ name }} {{ version }} has been removed from the [{{ repo.name|lower }}] repository.

    12 | 13 | {% if elsewhere %} 14 |

    However, this package or replacements are available elsewhere:

    15 |
      16 | {% for pkg in elsewhere %} 17 |
    • {% pkg_details_link pkg %} {{ pkg.full_version }} [{{ pkg.repo.name|lower }}] ({{ pkg.arch.name }})
    • 18 | {% endfor %} 19 |
    20 | {% else %} 21 |

    Unfortunately, this package cannot be found in any other repositories. 22 | Try using the package search page, 23 | or try searching the AUR 24 | to see if the package can be found there.

    25 | {% endif %} 26 |
    27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/packages/search_paginator.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if is_paginated %} 3 |

    {{ paginator.count }} matching packages found. 4 | Page {{ page_obj.number }} of {{ paginator.num_pages }}.

    5 | 6 |
    7 | 8 | {% if page_obj.has_previous %} 9 | 11 | {% else %} 12 | < Prev 13 | {% endif %} 14 | 15 | 16 | {% if page_obj.has_next %} 17 | 19 | {% else %} 20 | Next > 21 | {% endif %} 22 | 23 |
    24 | {% else %} 25 |

    {{ package_list|length }} matching package{{ package_list|pluralize }} found.

    26 | {% endif %} 27 |
    28 | -------------------------------------------------------------------------------- /templates/packages/signoff_cell.html: -------------------------------------------------------------------------------- 1 | {% if group.signoffs %} 2 |
      3 | {% for signoff in group.signoffs %} 4 |
    • {{ signoff.user }}{% if signoff.revoked %} (revoked){% endif %}
    • 5 | {% endfor %} 6 |
    7 | {% endif %} 8 | {% if group.user_signed_off %} 9 | 12 | {% else %} 13 | {% if not group.specification.known_bad and group.specification.enabled %} 14 |
    15 | Signoff
    17 | {% endif %} 18 | {% endif %} 19 | {% if user == group.packager or user in group.maintainers %} 20 |
    21 | Signoff Options 22 |
    23 | {% endif %} 24 | -------------------------------------------------------------------------------- /templates/packages/signoff_options.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Arch Linux - Package Signoff Options - {{ package.pkgbase }} {{ package.full_version }} ({{ package.arch.name }}){% endblock %} 4 | {% block head %}{% endblock %} 5 | {% block navbarclass %}anb-packages{% endblock %} 6 | 7 | {% block content %} 8 |
    9 |

    Package Signoff Options: {{ package.pkgbase }} {{ package.full_version }} ({{ package.arch.name }})

    10 |
    {% csrf_token %} 11 |
    12 | {{ form.as_p }} 13 |
    14 |

    15 |
    16 | 17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /templates/packages/sonames.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - {{ pkg.pkgname }} {{ pkg.full_version }} ({{ pkg.arch.name }}) - File List{% endblock %} 3 | {% block navbarclass %}anb-packages{% endblock %} 4 | 5 | {% block content %} 6 |
    7 | 8 |

    {{ pkg.pkgname }} {{ pkg.full_version }} Soname List

    9 |

    Back to Package

    10 |
    11 | {% include "packages/sonames_list.html" %} 12 |
    13 | 14 |
    15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/packages/sonames_list.html: -------------------------------------------------------------------------------- 1 | {% if sonames|length %} 2 |
      3 | {% for soname in sonames %} 4 |
    • {{ soname.name }}
    • {% endfor %} 5 |
    6 | {% else %} 7 |

    Package has no sonames.

    8 | {% endif %} 9 | -------------------------------------------------------------------------------- /templates/planet/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load cache %} 3 | {% load static %} 4 | 5 | {% block head %} 6 | 7 | {% endblock %} 8 | 9 | {% block content_left %} 10 | {% cache 62 planet-page-left %} 11 |
    12 |

    Arch Planet

    13 | 14 |

    Planet Arch Linux is a window into the world, work and 15 | lives of Arch Linux developers, package maintainers and support staff.

    16 |
    17 | 18 |
    19 | RSS Feed 21 | 22 | {% for entry in feed_items %} 23 |

    24 | {{ entry.title }} 26 |

    27 |

    {{ entry.publishdate|date:"Y-m-d" }}

    28 |
    29 | {{ entry.summary |safe }} 30 |
    31 | 32 | {% endfor %} 33 |
    34 | {% endcache %} 35 | {% endblock %} 36 | 37 | {% block content_right %} 38 | {% cache 115 planet-page-right %} 39 | 58 | {% endcache %} 59 | {% endblock %} 60 | -------------------------------------------------------------------------------- /templates/public/blank.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - Sample Page Title{% endblock %} 3 | 4 | {% block content %} 5 |
    6 |

    Sample page title

    7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /templates/public/userlist.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | {% load cache %} 4 | 5 | {% block title %}Arch Linux - {{ group.name }}{% endblock %} 6 | 7 | {% block head %}{% endblock %} 8 | 9 | {% block content %} 10 | {% cache 600 dev-tu-profiles group.name %} 11 |
    12 |

    Arch Linux {{ group.name }}

    13 | 14 |

    {{ group.description }}

    15 | 16 | {% with users as dev_list %} 17 | {% include 'public/developer_list.html' %} 18 | {% endwith %} 19 |
    20 | {% endcache %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Arch Linux - Developer Login{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    Developer Login

    9 | 10 |
    {% csrf_token %} 11 |
    12 | Please enter your credentials to login. 13 | {{ form.as_p }} 14 |

    15 |
    16 |
    17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /templates/registration/logout.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Arch Linux - Logout successful{% endblock %} 4 | 5 | {% block content %} 6 |
    7 |

    Developer Logout

    8 | 9 |

    Logout was successful. 10 | Click here to login again.

    11 |
    12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/sitemaps/news_sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for url in urlset %} 4 | {{ url.location }} 5 | {% if url.lastmod %}{{ url.lastmod|date:"Y-m-d" }}{% endif %} 6 | {% if url.changefreq %}{{ url.changefreq }}{% endif %} 7 | {% if url.priority %}{{ url.priority }}{% endif %} 8 | 9 | Arch Linux Newsen 10 | {% if url.item.postdate %}{{ url.item.postdate|date:"c" }}{% endif %} 11 | {% if url.item.title %}{{ url.item.title }}{% endif %} 12 | 13 | {% endfor %} 14 | 15 | -------------------------------------------------------------------------------- /templates/sitemaps/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% for url in urlset %} 4 | {{ url.location }} 5 | {% if url.lastmod %}{{ url.lastmod|date:"Y-m-d" }}{% endif %} 6 | {% if url.changefreq %}{{ url.changefreq }}{% endif %} 7 | {% if url.priority %}{{ url.priority }}{% endif %} 8 | {% endfor %} 9 | 10 | -------------------------------------------------------------------------------- /templates/todolists/complete_email_notification.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %}The todo list "{{ todolist.name }}" is complete. 2 | 3 | Todo list information: 4 | Name: {{ todolist.name }} 5 | URL: {{ todolist.get_full_url }} 6 | Creator: {{ todolist.creator.get_full_name }} 7 | Description: 8 | {{ todolist.description|striptags|wordwrap:78 }}{% endautoescape %} 9 | -------------------------------------------------------------------------------- /templates/todolists/email_notification.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %}The todo list "{{ todolist.name }}" has had the following packages added to it for which you are a maintainer: 2 | 3 | {% for tpkg in todo_packages %} 4 | * {{ tpkg.repo.name|lower }}/{{ tpkg.pkgname }} ({{ tpkg.arch.name }}) - {{ tpkg.pkg.get_full_url }}{% endfor %} 5 | 6 | Todo list information: 7 | Name: {{ todolist.name }} 8 | URL: {{ todolist.get_full_url }} 9 | Creator: {{ todolist.creator.get_full_name }} 10 | Description: 11 | {{ todolist.description|striptags|wordwrap:78 }}{% endautoescape %} 12 | -------------------------------------------------------------------------------- /templates/todolists/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Arch Linux - Todo Lists{% endblock %} 5 | 6 | {% block content %} 7 |
    8 | 9 |

    Package Todo Lists

    10 | 11 | {% if perms.todolists.add_todolist %}{% endif %} 14 | 15 |

    Todo lists are used by the developers when a rebuild of a set of 16 | packages is needed. This is common when a library has a version bump, 17 | during a toolchain rebuild, or a general cleanup of packages in the 18 | repositories. The progress can be tracked here, and completed todo lists 19 | can be browsed as well.

    20 | 21 | {% include "todolists/paginator.html" %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for list in lists %} 37 | 38 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | {% endfor %} 49 | 50 |
    NameCreation DateCreatorPackage CountIncomplete CountKindStatus
    {{ list.name }}{{ list.created|date:"Y-m-d" }}{{ list.creator.get_full_name }}{{ list.pkg_count }}{{ list.incomplete_count }}{{ list.kind_str }}{% if list.incomplete_count == 0 %}Complete 46 | {% else %}Incomplete{% endif %}
    51 | 52 | {% include "todolists/paginator.html" %} 53 |
    54 | {% endblock %} 55 | 56 | {% block script_block %} 57 | {% load cdn %}{% jquery %}{% jquery_tablesorter %} 58 | 59 | 64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /templates/todolists/paginator.html: -------------------------------------------------------------------------------- 1 | {% if is_paginated %} 2 | 22 | {% endif %} 23 | -------------------------------------------------------------------------------- /templates/todolists/todolist_confirm_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}Arch Linux - Delete Todo List: {{object.name}}{% endblock %} 3 | 4 | {% block content %} 5 |
    6 | 7 |

    Delete Todo List: {{object.name}}

    8 | 9 |

    You are about to delete the selected todo list:

    10 | 11 |
    12 |

    {{object.description|safe|linebreaks}}

    13 |
    14 | 15 |

    Are you sure?

    16 | 17 |
    {% csrf_token %} 18 |

    19 |
    20 | 21 |
    22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /templates/visualize/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load static %} 3 | 4 | {% block title %}Arch Linux - Visualizations{% endblock %} 5 | 6 | {% block content %} 7 |
    8 |

    Visualization of Package Data

    9 | 10 |
    11 |
    12 | Scale Using: 13 | 14 | 15 | 16 | 17 |
    18 |
    19 | Group By: 20 | 21 | 22 |
    23 |
    24 |
    25 |
    26 | {% endblock %} 27 | 28 | {% block script_block %} 29 | {% load cdn %}{% jquery %}{% d3js %} 30 | 31 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /todolists/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/todolists/__init__.py -------------------------------------------------------------------------------- /todolists/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Todolist 4 | 5 | 6 | class TodolistAdmin(admin.ModelAdmin): 7 | list_display = ('name', 'creator', 'created', 'description') 8 | list_filter = ('created', 'creator') 9 | search_fields = ('name', 'description') 10 | date_hierarchy = 'created' 11 | 12 | 13 | admin.site.register(Todolist, TodolistAdmin) 14 | 15 | # vim: set ts=4 sw=4 et: 16 | -------------------------------------------------------------------------------- /todolists/migrations/0002_todolist_kind.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1.5 on 2021-01-31 20:23 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('todolists', '0001_squashed_0002_remove_todolist_old_id'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='todolist', 15 | name='kind', 16 | field=models.SmallIntegerField(choices=[(0, 'Rebuild'), (1, 'Task')], default=0, 17 | help_text='(Rebuild for soname bumps, Task for independent tasks)'), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /todolists/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/todolists/migrations/__init__.py -------------------------------------------------------------------------------- /todolists/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/todolists/templatetags/__init__.py -------------------------------------------------------------------------------- /todolists/templatetags/todolists.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.utils.html import format_html 3 | 4 | register = template.Library() 5 | 6 | 7 | def pkg_absolute_url(repo, arch, pkgname): 8 | return '/packages/%s/%s/%s/' % (repo.name.lower(), arch.name, pkgname) 9 | 10 | 11 | @register.simple_tag 12 | def todopkg_details_link(todopkg): 13 | pkg = todopkg.pkg 14 | if not pkg: 15 | return todopkg.pkgname 16 | link = '%s' 17 | url = pkg_absolute_url(todopkg.repo, todopkg.arch, pkg.pkgname) 18 | return format_html(link % (url, pkg.pkgname, pkg.pkgname)) 19 | 20 | # vim: set ts=4 sw=4 et: 21 | -------------------------------------------------------------------------------- /todolists/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/todolists/tests/__init__.py -------------------------------------------------------------------------------- /todolists/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from main.models import Package 4 | from todolists.models import Todolist, TodolistPackage 5 | 6 | NAME = 'Boost rebuild' 7 | SLUG = 'boost-rebuild' 8 | DESCRIPTION = 'Boost 1.66 rebuild' 9 | RAW = 'linux' 10 | 11 | 12 | @pytest.fixture 13 | def todolist(user, arches, repos, package): 14 | todolist = Todolist.objects.create(name=NAME, 15 | description=DESCRIPTION, 16 | slug=SLUG, 17 | creator=user, 18 | raw=RAW) 19 | yield todolist 20 | todolist.delete() 21 | 22 | 23 | @pytest.fixture 24 | def todolistpackage(admin_user, todolist): 25 | pkg = Package.objects.first() 26 | todopkg = TodolistPackage.objects.create(pkg=pkg, pkgname=pkg.pkgname, 27 | pkgbase=pkg.pkgbase, arch=pkg.arch, 28 | repo=pkg.repo, user=admin_user, 29 | todolist=todolist) 30 | yield todopkg 31 | todopkg.delete() 32 | -------------------------------------------------------------------------------- /todolists/tests/test_models.py: -------------------------------------------------------------------------------- 1 | from main.models import Package 2 | from todolists.models import TodolistPackage 3 | from todolists.tests.conftest import NAME 4 | 5 | 6 | def test_stripped_description(todolist): 7 | todolist.description = 'Boost rebuild ' 8 | desc = todolist.stripped_description 9 | assert not desc.endswith(' ') 10 | 11 | 12 | def test_get_absolute_url(todolist): 13 | assert '/todo/' in todolist.get_absolute_url() 14 | 15 | 16 | def test_get_full_url(todolist): 17 | url = todolist.get_full_url() 18 | assert 'https://example.com/todo/' in url 19 | 20 | 21 | def test_packages(admin_user, todolist, todolistpackage): 22 | pkgs = todolist.packages() 23 | assert len(pkgs) == 1 24 | assert pkgs[0] == todolistpackage 25 | 26 | 27 | def test_str(admin_user, todolist): 28 | assert NAME in str(todolist) 29 | 30 | 31 | def test_todolist_str(admin_user, todolist, todolistpackage): 32 | assert todolistpackage.pkgname in str(todolistpackage) 33 | 34 | 35 | def test_status_css_class(admin_user, todolist, todolistpackage): 36 | assert todolistpackage.status_css_class() == 'incomplete' 37 | 38 | 39 | def test_status_str(admin_user, todolist, todolistpackage): 40 | assert todolistpackage.status_str == 'Incomplete' 41 | 42 | 43 | def test_todolist_complete(admin_user, todolist, todolistpackage, mailoutbox): 44 | pkg = Package.objects.last() 45 | todopkg = TodolistPackage.objects.create(pkg=pkg, pkgname=pkg.pkgname, 46 | pkgbase=pkg.pkgbase, arch=pkg.arch, 47 | repo=pkg.repo, user=admin_user, 48 | todolist=todolist, 49 | status=TodolistPackage.COMPLETE) 50 | assert todopkg 51 | assert len(mailoutbox) == 0 52 | todolistpackage.status = TodolistPackage.COMPLETE 53 | todolistpackage.save() 54 | assert len(mailoutbox) == 1 55 | todopkg.delete() 56 | -------------------------------------------------------------------------------- /todolists/tests/test_templatetags_todolists.py: -------------------------------------------------------------------------------- 1 | from todolists.templatetags.todolists import todopkg_details_link 2 | 3 | 4 | def test_details_link(todolistpackage): 5 | link = todopkg_details_link(todolistpackage) 6 | assert f'View package details for {todolistpackage.pkg.pkgname}' in link 7 | -------------------------------------------------------------------------------- /todolists/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.decorators import permission_required 2 | from django.urls import path, re_path 3 | 4 | from .views import ( 5 | DeleteTodolist, 6 | TodolistListView, 7 | add, 8 | edit, 9 | flag, 10 | list_pkgbases, 11 | view, 12 | view_json, 13 | ) 14 | 15 | urlpatterns = [ 16 | path('', TodolistListView.as_view(), name='todolist-list'), 17 | 18 | path('add/', 19 | permission_required('todolists.add_todolist')(add)), 20 | re_path(r'^(?P[-\w]+)/$', view), 21 | re_path(r'^(?P[-\w]+)/json$', view_json), 22 | re_path(r'^(?P[-\w]+)/edit/$', 23 | permission_required('todolists.change_todolist')(edit)), 24 | re_path(r'^(?P[-\w]+)/delete/$', 25 | permission_required('todolists.delete_todolist')(DeleteTodolist.as_view())), 26 | re_path(r'^(?P[-\w]+)/flag/(?P\d+)/$', 27 | permission_required('todolists.change_todolistpackage')(flag)), 28 | re_path(r'^(?P[-\w]+)/pkgbases/(?P[a-z]+)/$', 29 | list_pkgbases), 30 | ] 31 | 32 | # vim: set ts=4 sw=4 et: 33 | -------------------------------------------------------------------------------- /todolists/utils.py: -------------------------------------------------------------------------------- 1 | from django.db import connections, router 2 | 3 | from packages.models import Package 4 | 5 | from .models import Todolist, TodolistPackage 6 | 7 | 8 | def todo_counts(): 9 | sql = """ 10 | SELECT todolist_id, count(*), SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) 11 | FROM todolists_todolistpackage 12 | WHERE removed IS NULL 13 | GROUP BY todolist_id 14 | """ 15 | database = router.db_for_write(TodolistPackage) 16 | connection = connections[database] 17 | cursor = connection.cursor() 18 | cursor.execute(sql, [TodolistPackage.COMPLETE]) 19 | results = cursor.fetchall() 20 | return {row[0]: (row[1], row[2]) for row in results} 21 | 22 | 23 | def get_annotated_todolists(incomplete_only=False): 24 | lists = Todolist.objects.all().defer('raw').select_related( 25 | 'creator').order_by('-created') 26 | lookup = todo_counts() 27 | 28 | # tag each list with package counts 29 | for todolist in lists: 30 | counts = lookup.get(todolist.id, (0, 0)) 31 | todolist.pkg_count = counts[0] 32 | todolist.complete_count = counts[1] 33 | todolist.incomplete_count = counts[0] - counts[1] 34 | 35 | if incomplete_only: 36 | lists = [lst for lst in lists if lst.incomplete_count > 0] 37 | else: 38 | lists = sorted(lists, key=lambda todolist: todolist.incomplete_count == 0) 39 | return lists 40 | 41 | 42 | def attach_staging(packages, list_id): 43 | '''Look for any staging version of the packages provided and attach them 44 | to the 'staging' attribute on each package if found.''' 45 | pkgnames = TodolistPackage.objects.filter( 46 | todolist_id=list_id).values('pkgname') 47 | staging_pkgs = Package.objects.normal().filter(repo__staging=True, 48 | pkgname__in=pkgnames) 49 | # now build a lookup dict to attach to the correct package 50 | lookup = {(p.pkgname, p.arch): p for p in staging_pkgs} 51 | 52 | annotated = [] 53 | for package in packages: 54 | in_staging = lookup.get((package.pkgname, package.arch), None) 55 | package.staging = in_staging 56 | 57 | return annotated 58 | 59 | # vim: set ts=4 sw=4 et: 60 | -------------------------------------------------------------------------------- /visualize/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/archlinux/archweb/c1d70301c94d52e59f4f621387e046052f94e3ae/visualize/__init__.py -------------------------------------------------------------------------------- /visualize/tests/test_urls.py: -------------------------------------------------------------------------------- 1 | def test_urls(client, arches, repos, package): 2 | for url in ['', 'by_repo/', 'by_arch/']: 3 | response = client.get(f'/visualize/{url}') 4 | assert response.status_code == 200 5 | -------------------------------------------------------------------------------- /visualize/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from visualize import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='visualize-index'), 7 | path('by_arch/', views.by_arch, name='visualize-byarch'), 8 | path('by_repo/', views.by_repo, name='visualize-byrepo'), 9 | ] 10 | 11 | # vim: set ts=4 sw=4 et: 12 | -------------------------------------------------------------------------------- /visualize/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.db.models import Count, Sum 4 | from django.http import HttpResponse 5 | from django.shortcuts import render 6 | from django.views.decorators.cache import cache_page 7 | 8 | from main.models import Arch, Package, Repo 9 | 10 | 11 | def index(request): 12 | return render(request, 'visualize/index.html') 13 | 14 | 15 | def arch_repo_data(): 16 | qs = Package.objects.select_related().values( 17 | 'arch__name', 'repo__name').annotate( 18 | count=Count('pk'), csize=Sum('compressed_size'), 19 | isize=Sum('installed_size'), 20 | flagged=Count('flag_date')).order_by() 21 | arches = Arch.objects.values_list('name', flat=True) 22 | repos = Repo.objects.values_list('name', flat=True) 23 | 24 | def build_map(name, arch, repo): 25 | key = '%s:%s' % (repo or '', arch or '') 26 | return { 27 | 'key': key, 28 | 'name': name, 29 | 'arch': arch, 30 | 'repo': repo, 31 | 'data': [], 32 | } 33 | 34 | # now transform these results into two mappings: one ordered (repo, arch), 35 | # and one ordered (arch, repo). 36 | arch_groups = {a: build_map(a, a, None) for a in arches} 37 | repo_groups = {r: build_map(r, None, r) for r in repos} 38 | for row in qs: 39 | arch = row['arch__name'] 40 | repo = row['repo__name'] 41 | values = { 42 | 'arch': arch, 43 | 'repo': repo, 44 | 'name': '%s (%s)' % (repo, arch), 45 | 'key': '%s:%s' % (repo, arch), 46 | 'csize': row['csize'], 47 | 'isize': row['isize'], 48 | 'count': row['count'], 49 | 'flagged': row['flagged'], 50 | } 51 | arch_groups[arch]['data'].append(values) 52 | repo_groups[repo]['data'].append(values) 53 | 54 | data = { 55 | 'by_arch': {'name': 'Architectures', 'data': list(arch_groups.values())}, 56 | 'by_repo': {'name': 'Repositories', 'data': list(repo_groups.values())}, 57 | } 58 | return data 59 | 60 | 61 | @cache_page(1800) 62 | def by_arch(request): 63 | data = arch_repo_data() 64 | to_json = json.dumps(data['by_arch'], ensure_ascii=False) 65 | return HttpResponse(to_json, content_type='application/json') 66 | 67 | 68 | @cache_page(1800) 69 | def by_repo(request): 70 | data = arch_repo_data() 71 | to_json = json.dumps(data['by_repo'], ensure_ascii=False) 72 | return HttpResponse(to_json, content_type='application/json') 73 | 74 | # vim: set ts=4 sw=4 et: 75 | --------------------------------------------------------------------------------