├── .nvmrc ├── csp ├── .gitignore ├── README.md ├── tsconfig.json ├── package.json └── src │ └── main.ts ├── ui-benchmark ├── .gitignore ├── package.json ├── login.js ├── .lighthouserc.js ├── metrics.sh └── README.md ├── .env.example ├── accessibility ├── data │ └── .gitignore ├── old │ ├── jest.config.js │ ├── axe.puppeteer.js │ └── index.test.js ├── docs │ ├── axe-htmlcs-mapping.json │ ├── success-criteria-mapping.js │ └── linting.json ├── pa11y-report.js ├── pa11y-sc-report.js ├── pa11y-dedupe.js ├── pa11y-test.js └── report-html.js ├── .eslintignore ├── .prettierignore ├── http-archive ├── crux │ ├── all-latest_crux_desktop.sql │ └── wagtail-crux-2023_04_01.sql ├── gaad-2022 │ └── wagtail-lighthouse-2022_05_01.sql ├── gaad-2023 │ ├── wagtail-lighthouse-2023_04_01.sql │ ├── README.md │ ├── wagtail-lighthouse-scores-2023_04_01.sql │ ├── LH audit scores 2023-04-01.sql │ ├── Django LH audit scores 2023-04-01.sql │ ├── Wagtail LH audit scores 2023_04_01_desktop.sql │ └── Django LH audit scores 2023_08_01_desktop.sql ├── legacy │ ├── Wagtail page weights.sql │ ├── django-lighthouse-scores-2023_04_01_desktop.sql │ ├── django-wagtail-lighthouse-2024_04_01.sql │ ├── django-lighthouse-scores-2023_08_01_desktop.sql │ ├── README.md │ ├── drupal-lighthouse-scores.sql │ ├── perfect-lighthouse-a11y-django-sites.sql │ ├── Wagtail LH audit scores 2023_08_01_desktop.sql │ ├── Wagtail LH perf audit scores 2024_04_01_desktop.sql │ ├── LH audit scores per URL Django-Wagtail 2024-04-01.sql │ └── Tranco site ranking.sql ├── sustainability │ ├── Django Wagtail CruX weights 2024_04_19.sql │ └── wagtail-crux-weights-2025.sql ├── aria-label │ ├── package.json │ ├── README.md │ ├── main.js │ └── package-lock.json ├── gaad-2024 │ ├── all-wagtail-lighthouse.sql │ ├── README.md │ ├── wagtail-lighthouse-scores-2024_04_01.sql │ ├── wagtail-lighthouse-scores-2024-only.sql │ └── wagtail-lighthouse-scores-2023-2024.sql └── gaad-2025 │ ├── wagtail-reports.sql │ ├── desktop-wagtail-lighthouse.sql │ ├── wagtail-lighthouse-scores-2025_04_01.sql │ ├── wagtail-lighthouse-scores-2025-only.sql │ ├── wagtail-lighthouse-scores-2024-2025.sql │ ├── README.md │ └── lh-audit-scores.sql ├── docs-screenshots └── imageStub.jpg ├── backstop ├── engine_scripts │ ├── imageStub.jpg │ ├── playwright │ │ ├── onBefore.js │ │ ├── onReady.js │ │ ├── overrideCSS.js │ │ ├── interceptImages.js │ │ ├── loadCookies.js │ │ └── clickAndHoverHelper.js │ └── puppeteer │ │ ├── onBefore.js │ │ ├── clickSelector.js │ │ ├── overrideCSS.js │ │ ├── requestOverrides.js │ │ ├── interceptImages.js │ │ ├── loadCookies.js │ │ ├── ignoreCSP.js │ │ ├── clickAndHoverHelper.js │ │ └── onReady.js └── backstop.config.js ├── downloads-analysis ├── versions-share │ ├── versions-over-time-daily.csv.zst │ ├── README.md │ ├── version-share-now.sql │ ├── versions-over-time-daily.sql │ ├── version-share-over-time.sql │ └── pivot_downloads.sql ├── installers-stats.sql ├── ci-installers-stats.sql ├── release-candidate-downloads.csv ├── new-packages-this-week.sql ├── README.md ├── release-candidate-total.sql ├── release-candidate-downloads.sql ├── installer-stats.csv └── ci-installer-stats.csv ├── .eslintrc.js ├── prettier.config.js ├── .editorconfig ├── contrast-themes └── backstop.config.js ├── LICENSE ├── package.json ├── ui ├── export-figma.js ├── export-components.js ├── export-scenarios.js └── docs │ └── ui-overview.tsv ├── .gitignore └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 24 2 | -------------------------------------------------------------------------------- /csp/.gitignore: -------------------------------------------------------------------------------- 1 | storage 2 | -------------------------------------------------------------------------------- /ui-benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | .lighthouseci 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | WAGTAIL_SESSIONID=your_sessionid_cookie_value 2 | -------------------------------------------------------------------------------- /accessibility/data/.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.json 3 | *.png 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.min.js 3 | coverage/ 4 | dist/ 5 | es/ 6 | *.bundle.js 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.min.js 3 | coverage/ 4 | dist/ 5 | es/ 6 | *.bundle.js 7 | -------------------------------------------------------------------------------- /accessibility/old/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "jest-puppeteer", 3 | }; 4 | -------------------------------------------------------------------------------- /http-archive/crux/all-latest_crux_desktop.sql: -------------------------------------------------------------------------------- 1 | SELECT url, rank FROM `httparchive.urls.latest_crux_desktop` 2 | -------------------------------------------------------------------------------- /docs-screenshots/imageStub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thibaudcolas/wagtail-tooling/HEAD/docs-screenshots/imageStub.jpg -------------------------------------------------------------------------------- /backstop/engine_scripts/imageStub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thibaudcolas/wagtail-tooling/HEAD/backstop/engine_scripts/imageStub.jpg -------------------------------------------------------------------------------- /csp/README.md: -------------------------------------------------------------------------------- 1 | # CSP scanner set up with [crawlee and Playwright](https://crawlee.dev/api/playwright-crawler/class/PlaywrightCrawler) 2 | 3 | ```bash 4 | npm run start 5 | ``` 6 | -------------------------------------------------------------------------------- /downloads-analysis/versions-share/versions-over-time-daily.csv.zst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thibaudcolas/wagtail-tooling/HEAD/downloads-analysis/versions-share/versions-over-time-daily.csv.zst -------------------------------------------------------------------------------- /backstop/engine_scripts/playwright/onBefore.js: -------------------------------------------------------------------------------- 1 | module.exports = async (page, scenario, viewport, isReference, browserContext) => { 2 | await require('./loadCookies')(browserContext, scenario); 3 | }; 4 | -------------------------------------------------------------------------------- /http-archive/crux/wagtail-crux-2023_04_01.sql: -------------------------------------------------------------------------------- 1 | SELECT url, rank 2 | FROM `httparchive.technologies.2023_04_01_desktop` 3 | JOIN `httparchive.urls.latest_crux_desktop` 4 | USING (url) 5 | WHERE app = 'Wagtail' 6 | ORDER BY rank 7 | -------------------------------------------------------------------------------- /backstop/engine_scripts/playwright/onReady.js: -------------------------------------------------------------------------------- 1 | module.exports = async (page, scenario, viewport, isReference, browserContext) => { 2 | console.log('SCENARIO > ' + scenario.label); 3 | await require('./clickAndHoverHelper')(page, scenario); 4 | 5 | // add more ready handlers here... 6 | }; 7 | -------------------------------------------------------------------------------- /csp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@apify/tsconfig", 3 | "compilerOptions": { 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "target": "ES2022", 7 | "outDir": "dist", 8 | "noUnusedLocals": false, 9 | "lib": ["DOM"] 10 | }, 11 | "include": ["./src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parserOptions: { 3 | ecmaVersion: 2017, 4 | }, 5 | 6 | env: { 7 | es6: true, 8 | node: true, 9 | jest: true, 10 | browser: true, 11 | }, 12 | 13 | extends: "eslint:recommended", 14 | 15 | rules: { 16 | "no-console": "off", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /http-archive/gaad-2022/wagtail-lighthouse-2022_05_01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse reports for Wagtail 3 | SELECT 4 | url, 5 | report 6 | FROM 7 | `httparchive.lighthouse.2022_05_01_desktop` 8 | JOIN 9 | `httparchive.technologies.2022_05_01_desktop` 10 | USING 11 | (url) 12 | WHERE 13 | app = 'Wagtail' 14 | -------------------------------------------------------------------------------- /http-archive/gaad-2023/wagtail-lighthouse-2023_04_01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse reports for Wagtail 3 | SELECT 4 | url, 5 | report 6 | FROM 7 | `httparchive.lighthouse.2023_04_01_desktop` 8 | JOIN 9 | `httparchive.technologies.2023_04_01_desktop` 10 | USING 11 | (url) 12 | WHERE 13 | app = 'Wagtail' 14 | -------------------------------------------------------------------------------- /http-archive/legacy/Wagtail page weights.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | DISTINCT(url), 3 | crux.rank, 4 | bytesTotal / 1024 AS total_kb 5 | FROM `wagtail-analysis.wagtail_httparchive.wagtail-crux-2023_04_01` as crux 6 | JOIN 7 | `httparchive.summary_pages.2023_04_01_desktop` 8 | USING 9 | (url) 10 | ORDER BY 11 | rank 12 | -------------------------------------------------------------------------------- /http-archive/sustainability/Django Wagtail CruX weights 2024_04_19.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | url, 3 | rank, 4 | bytesTotal / 1024 AS total_kb 5 | FROM `wagtail_httparchive.2024_04_19_django_wagtail_all_urls` as urls 6 | JOIN 7 | `httparchive.summary_pages.2024_04_01_desktop` 8 | USING 9 | (url) 10 | ORDER BY 11 | rank 12 | -------------------------------------------------------------------------------- /http-archive/legacy/django-lighthouse-scores-2023_04_01_desktop.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse reports for Django 3 | SELECT 4 | url, 5 | report 6 | FROM 7 | `httparchive.lighthouse.2023_04_01_desktop` 8 | JOIN 9 | `httparchive.technologies.2023_04_01_desktop` 10 | USING 11 | (url) 12 | WHERE 13 | app = 'Django' 14 | -------------------------------------------------------------------------------- /http-archive/aria-label/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aria-label", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "keywords": [], 8 | "author": "", 9 | "license": "MIT", 10 | "dependencies": { 11 | "aria-query": "^5.3.2", 12 | "cheerio": "^1.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /http-archive/legacy/django-wagtail-lighthouse-2024_04_01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse reports for Django & Wagtail projects 3 | SELECT 4 | url, 5 | report 6 | FROM 7 | `httparchive.lighthouse.2024_04_01_desktop` 8 | JOIN 9 | `wagtail-analysis.wagtail_httparchive.2024_04_19_django_wagtail_all_urls` 10 | USING 11 | (url) 12 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // See https://prettier.io/docs/en/options.html. 2 | module.exports = { 3 | printWidth: 80, 4 | tabWidth: 2, 5 | useTabs: false, 6 | semi: true, 7 | singleQuote: false, 8 | trailingComma: "all", 9 | bracketSpacing: true, 10 | jsxBracketSameLine: false, 11 | arrowParens: "always", 12 | proseWrap: "preserve", 13 | }; 14 | -------------------------------------------------------------------------------- /http-archive/legacy/django-lighthouse-scores-2023_08_01_desktop.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse reports for Django 3 | SELECT 4 | url, 5 | report, 6 | app 7 | FROM 8 | `httparchive.lighthouse.2023_08_01_desktop` 9 | JOIN 10 | `httparchive.technologies.2023_08_01_desktop` 11 | USING 12 | (url) 13 | WHERE 14 | app = 'Django' 15 | or app = 'Wagtail' 16 | -------------------------------------------------------------------------------- /ui-benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-benchmark", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "devDependencies": { 13 | "@lhci/cli": "^0.14.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /http-archive/gaad-2024/all-wagtail-lighthouse.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse reports for Wagtail 3 | # Large query to save as a table for further, cheaper querying 4 | SELECT 5 | url, 6 | report 7 | FROM 8 | `httparchive.lighthouse.2024_01_01_desktop` 9 | JOIN 10 | `httparchive.technologies.2024_01_01_desktop` 11 | USING 12 | (url) 13 | WHERE 14 | app = 'Wagtail' 15 | -------------------------------------------------------------------------------- /downloads-analysis/versions-share/README.md: -------------------------------------------------------------------------------- 1 | # Wagtail versions share 2 | 3 | ```sql 4 | create table dl as select * from './versions-over-time-daily.csv.zst'; 5 | create table feature_versions as select 6 | day, 7 | regexp_extract(version, '^(\d+\.\d+)') as minor_version, 8 | sum(num_downloads) as total_downloads 9 | from dl 10 | group by day, minor_version 11 | order by day, minor_version; 12 | ``` 13 | -------------------------------------------------------------------------------- /http-archive/gaad-2025/wagtail-reports.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | SELECT 3 | * 4 | FROM 5 | `wagtail-analysis.wagtail_httparchive.2025_04_01_django_wagtail_reports` AS w 6 | JOIN 7 | `httparchive.crawl.pages` AS p 8 | ON p.page = w.page 9 | WHERE 10 | p.date = DATE '2025-04-01' -- † partition pruning 11 | AND p.client = 'desktop' -- † part of the clustering key 12 | ORDER BY 13 | w.rank; 14 | -------------------------------------------------------------------------------- /http-archive/legacy/README.md: -------------------------------------------------------------------------------- 1 | # Legacy queries an data 2 | 3 | One-off queries and other experiments that didn’t work out. 4 | 5 | ## Deleted tables 6 | 7 | ``` 8 | wagtail-lighthouse-2022_05_01 9 | wagtail-lighthouse-2023_04_01 10 | wagtail-lighthouse-2023_04_01-snapshot 11 | wagtail-crux-2023_04_01 12 | all-latest_crux_desktop 13 | django-lighthouse-2023_04_01_desktop 14 | drupal-lighthouse-2023_04_01_desktop 15 | django-wagtail-lighthouse-2023_08_01_desktop 16 | ``` 17 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/onBefore.js: -------------------------------------------------------------------------------- 1 | module.exports = async (page, scenario, vp) => { 2 | await require("./loadCookies")(page, scenario); 3 | 4 | if (scenario.emulateVisionDeficiency) { 5 | await page.emulateVisionDeficiency(scenario.emulateVisionDeficiency); 6 | } 7 | 8 | if (scenario.emulateMediaFeatures) { 9 | const client = await page.target().createCDPSession(); 10 | await client.send("Emulation.setEmulatedMedia", { 11 | features: scenario.emulateMediaFeatures, 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /http-archive/legacy/drupal-lighthouse-scores.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages, 2023-04-01 data, 3 | SELECT 4 | url, 5 | JSON_EXTRACT_SCALAR(report, '$.categories.accessibility.score') AS accessibility, 6 | JSON_EXTRACT_SCALAR(report, '$.categories.performance.score') AS performance, 7 | JSON_EXTRACT_SCALAR(report, '$.categories.seo.score') AS seo, 8 | JSON_EXTRACT_SCALAR(report, '$.categories.best-practices.score') AS best_practices, 9 | FROM 10 | `wagtail_httparchive.drupal-lighthouse-2023_04_01_desktop` 11 | -------------------------------------------------------------------------------- /http-archive/legacy/perfect-lighthouse-a11y-django-sites.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse category scores per CMS 3 | SELECT 4 | app as cms, 5 | COUNT(DISTINCT url) AS freq 6 | FROM 7 | `httparchive.lighthouse.2023_08_01_desktop` 8 | JOIN 9 | `httparchive.technologies.2023_08_01_desktop` 10 | USING 11 | (url) 12 | WHERE 13 | category = 'Web frameworks' 14 | AND 15 | CAST(JSON_EXTRACT_SCALAR(report, '$.categories.accessibility.score') AS NUMERIC) = 1 16 | AND ( 17 | app = 'Django' 18 | ) 19 | GROUP BY 20 | app 21 | ORDER BY 22 | freq 23 | -------------------------------------------------------------------------------- /http-archive/gaad-2023/README.md: -------------------------------------------------------------------------------- 1 | # Wagtail sites accessibility GAAD 2023 2 | 3 | Spreadsheet: [Wagtail sites accessibility GAAD 2023](https://docs.google.com/spreadsheets/d/1dLpW6fbcl-AsVQNVhihzi1p-fY5gByZK_EQWt-EtCoM/edit) 4 | 5 | Data sources (all via HTTPArchive BigQuery): 6 | 7 | https://developer.chrome.com/docs/crux/ 8 | https://httparchive.org/ 9 | https://www.wappalyzer.com/ 10 | 11 | Methodology references: 12 | 13 | https://almanac.httparchive.org/en/2022/accessibility 14 | 15 | 👉 Key results 16 | 17 | https://wagtail.org/gaad-2023-stats/ 18 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/clickSelector.js: -------------------------------------------------------------------------------- 1 | module.exports = async (page, scenario, viewport) => { 2 | // console.log(`clickSelector: ${scenario.label} @${viewport.label}`); 3 | let clickSelector = scenario.clickSelector; 4 | 5 | if (!Array.isArray(clickSelector)) { 6 | clickSelector = [clickSelector]; 7 | } 8 | 9 | for (const selector of [].concat(clickSelector)) { 10 | await page.click(selector); 11 | await page.waitForTimeout(500); 12 | } 13 | 14 | if (scenario.onReadySelector) { 15 | await page.waitForSelector(scenario.onReadySelector); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /ui-benchmark/login.js: -------------------------------------------------------------------------------- 1 | // Untested! 2 | /** 3 | * @param {puppeteer.Browser} browser 4 | * @param {{url: string, options: LHCI.CollectCommand.Options}} context 5 | */ 6 | module.exports = async (browser, context) => { 7 | // launch browser for LHCI 8 | const page = await browser.newPage(); 9 | await page.goto("http://localhost:8080/admin/login/"); 10 | await page.type("#username", "admin"); 11 | await page.type("#password", "changeme"); 12 | await page.click('[type="submit"]'); 13 | await page.waitForNavigation(); 14 | // close session for next run 15 | await page.close(); 16 | }; 17 | -------------------------------------------------------------------------------- /downloads-analysis/versions-share/version-share-now.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | SELECT 3 | downloads.file.version AS version, 4 | COUNT(*) AS downloads_count 5 | FROM 6 | `bigquery-public-data.pypi.file_downloads` AS downloads 7 | WHERE 8 | downloads.project = 'wagtail' 9 | AND (SUBSTRING(downloads.file.version,0,1) = '2' 10 | OR SUBSTRING(downloads.file.version,0,1) = '3' 11 | OR SUBSTRING(downloads.file.version,0,1) = '4') 12 | AND (DATE(downloads.timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 day) 13 | AND CURRENT_DATE()) 14 | GROUP BY 15 | version 16 | ORDER BY 17 | version desc 18 | -------------------------------------------------------------------------------- /downloads-analysis/versions-share/versions-over-time-daily.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | -- BigQuery – downloads of the Wagtail project by version by day 3 | WITH filtered_downloads AS ( 4 | SELECT 5 | dl.timestamp, 6 | dl.file.version AS version 7 | FROM `bigquery-public-data.pypi.file_downloads` as dl 8 | WHERE project = 'wagtail' 9 | AND dl.timestamp BETWEEN TIMESTAMP('2016-01-01T00:00:01.000Z') AND CURRENT_TIMESTAMP() 10 | ) 11 | SELECT 12 | TIMESTAMP_TRUNC(timestamp, DAY) AS day, 13 | version, 14 | COUNT(*) AS num_downloads 15 | FROM filtered_downloads 16 | GROUP BY day, version 17 | ORDER BY day, num_downloads DESC; 18 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/overrideCSS.js: -------------------------------------------------------------------------------- 1 | const BACKSTOP_TEST_CSS_OVERRIDE = "html {background-image: none;}"; 2 | 3 | module.exports = async (page, scenario) => { 4 | // inject arbitrary css to override styles 5 | await page.evaluate(`window._styleData = '${BACKSTOP_TEST_CSS_OVERRIDE}'`); 6 | await page.evaluate(() => { 7 | const style = document.createElement("style"); 8 | const styleNode = document.createTextNode(window._styleData); 9 | style.appendChild(styleNode); 10 | document.head.appendChild(style); 11 | }); 12 | 13 | console.log("BACKSTOP_TEST_CSS_OVERRIDE injected for: " + scenario.label); 14 | }; 15 | -------------------------------------------------------------------------------- /http-archive/gaad-2024/README.md: -------------------------------------------------------------------------------- 1 | # Wagtail sites accessibility GAAD 2024 2 | 3 | Spreadsheet: [Wagtail sites accessibility GAAD 2024](https://docs.google.com/spreadsheets/d/1hQXCSbvAtmdf7IArBT4RL3cvgldCUx1UPGzABC_g8Dc/edit) 4 | 5 | Data sources (all via HTTPArchive BigQuery): 6 | 7 | https://developer.chrome.com/docs/crux/ 8 | https://httparchive.org/ 9 | https://www.wappalyzer.com/ 10 | 11 | Methodology references: 12 | 13 | https://almanac.httparchive.org/en/2022/accessibility 14 | 15 | 👉 Key results 16 | 17 | [Wagtail accessibility statistics for GAAD 2024](https://wagtail.org/blog/wagtail-accessibility-statistics-for-gaad-2024/) 18 | -------------------------------------------------------------------------------- /http-archive/gaad-2025/desktop-wagtail-lighthouse.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | -- Query estimated to 20TB of billable data processed, but this is likely to be an overestimate due to https://har.fyi/guides/minimizing-costs/. 3 | -- Effective bytes billed: 5TB 4 | SELECT 5 | page, 6 | rank, 7 | summary, 8 | custom_metrics.a11y, 9 | lighthouse, 10 | technologies 11 | FROM 12 | `httparchive.crawl.pages` 13 | WHERE 14 | date = '2025-04-01' 15 | AND client = 'desktop' 16 | AND is_root_page = true 17 | AND EXISTS ( 18 | SELECT 1 FROM UNNEST(technologies) AS tech 19 | WHERE tech.technology = 'Wagtail' or tech.technology = 'Django' 20 | ) 21 | ORDER BY rank ASC; 22 | -------------------------------------------------------------------------------- /http-archive/gaad-2023/wagtail-lighthouse-scores-2023_04_01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages, 2023-04-01 data, 3 | SELECT 4 | url, 5 | rank, 6 | JSON_EXTRACT_SCALAR(report, '$.categories.accessibility.score') AS accessibility, 7 | JSON_EXTRACT_SCALAR(report, '$.categories.performance.score') AS performance, 8 | JSON_EXTRACT_SCALAR(report, '$.categories.seo.score') AS seo, 9 | JSON_EXTRACT_SCALAR(report, '$.categories.best-practices.score') AS best_practices, 10 | FROM 11 | `wagtail_httparchive.wagtail-lighthouse-2023_04_01` 12 | JOIN 13 | `wagtail_httparchive.wagtail-crux-2023_04_01` 14 | USING 15 | (url) 16 | ORDER BY 17 | rank 18 | -------------------------------------------------------------------------------- /http-archive/gaad-2024/wagtail-lighthouse-scores-2024_04_01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages, 2024-04-01 data 3 | SELECT 4 | url, 5 | JSON_EXTRACT_SCALAR(report, '$.categories.accessibility.score') AS accessibility, 6 | JSON_EXTRACT_SCALAR(report, '$.categories.performance.score') AS performance, 7 | JSON_EXTRACT_SCALAR(report, '$.categories.seo.score') AS seo, 8 | JSON_EXTRACT_SCALAR(report, '$.categories.best-practices.score') AS best_practices, 9 | FROM 10 | `wagtail-analysis.wagtail_httparchive.2024_04_19_wagtail_crux_urls` 11 | JOIN 12 | `wagtail-analysis.wagtail_httparchive.django-wagtail-lighthouse-2024_04_01_desktop` 13 | USING (url) 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Defines the coding style for different editors and IDEs. 2 | # https://editorconfig.org/ 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Rules for source code. 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | max_line_length = 80 16 | 17 | # Documentation. 18 | [*.md] 19 | max_line_length = 0 20 | trim_trailing_whitespace = false 21 | 22 | # Git commit messages. 23 | [COMMIT_EDITMSG] 24 | max_line_length = 0 25 | trim_trailing_whitespace = false 26 | 27 | # Makefiles require tabs. 28 | [Makefile] 29 | indent_style = tab 30 | indent_size = 8 31 | -------------------------------------------------------------------------------- /contrast-themes/backstop.config.js: -------------------------------------------------------------------------------- 1 | const baseConfig = require("../backstop/backstop.config"); 2 | 3 | module.exports = { 4 | ...baseConfig, 5 | paths: { 6 | ...baseConfig.paths, 7 | bitmaps_reference: `${__dirname}/data/bitmaps_reference`, 8 | bitmaps_test: `${__dirname}/data/bitmaps_test`, 9 | html_report: `${__dirname}/data/html_report`, 10 | ci_report: `${__dirname}/data/ci_report`, 11 | }, 12 | scenarios: baseConfig.scenarios.map((s) => ({ 13 | ...s, 14 | // emulateVisionDeficiency: "achromatopsia", 15 | // emulateMediaFeatures: [ 16 | // { name: "forced-colors", value: "active" }, 17 | // { name: "prefers-contrast", value: "more" }, 18 | // ], 19 | })), 20 | }; 21 | -------------------------------------------------------------------------------- /backstop/engine_scripts/playwright/overrideCSS.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OVERRIDE CSS 3 | * Apply this CSS to the loaded page, as a way to override styles. 4 | * 5 | * Use this in an onReady script E.G. 6 | ``` 7 | module.exports = async function(page, scenario) { 8 | await require('./overrideCSS')(page, scenario); 9 | } 10 | ``` 11 | * 12 | */ 13 | 14 | const BACKSTOP_TEST_CSS_OVERRIDE = ` 15 | html { 16 | background-image: none; 17 | } 18 | `; 19 | 20 | module.exports = async (page, scenario) => { 21 | // inject arbitrary css to override styles 22 | await page.addStyleTag({ 23 | content: BACKSTOP_TEST_CSS_OVERRIDE 24 | }); 25 | 26 | console.log('BACKSTOP_TEST_CSS_OVERRIDE injected for: ' + scenario.label); 27 | }; 28 | -------------------------------------------------------------------------------- /csp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csp", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "description": "This is an example of a Crawlee project.", 6 | "dependencies": { 7 | "crawlee": "^3.0.0", 8 | "playwright": "*" 9 | }, 10 | "devDependencies": { 11 | "@apify/tsconfig": "^0.1.0", 12 | "tsx": "^4.4.0", 13 | "typescript": "~5.7.0", 14 | "@types/node": "^22.0.0" 15 | }, 16 | "scripts": { 17 | "start": "npm run start:dev", 18 | "start:prod": "node dist/main.js", 19 | "start:dev": "tsx src/main.ts", 20 | "build": "tsc", 21 | "test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1", 22 | "postinstall": "npx crawlee install-playwright-browsers" 23 | }, 24 | "license": "ISC" 25 | } 26 | -------------------------------------------------------------------------------- /http-archive/gaad-2025/wagtail-lighthouse-scores-2025_04_01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages, 2025-04-01 data 3 | SELECT 4 | page, 5 | rank, 6 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.accessibility.score') AS accessibility, 7 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.performance.score') AS performance, 8 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.seo.score') AS seo, 9 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.best-practices.score') AS best_practices, 10 | FROM 11 | `wagtail-analysis.wagtail_httparchive.2025_04_01_django_wagtail_reports` 12 | WHERE 13 | lighthouse IS NOT NULL 14 | AND EXISTS (SELECT 1 FROM UNNEST(technologies) AS tech WHERE tech.technology = 'Wagtail') 15 | ORDER BY rank ASC 16 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/requestOverrides.js: -------------------------------------------------------------------------------- 1 | const wagtailUpgradeAvailable = { 2 | "https://releases.wagtail.org/latest.txt": { 3 | status: 200, 4 | contentType: "binary/octet-stream", 5 | headers: { 6 | "access-control-allow-methods": "GET", 7 | "access-control-allow-origin": "*", 8 | }, 9 | body: JSON.stringify({ 10 | version: "999", 11 | url: "https://docs.wagtail.org/en/v999/releases/999.html", 12 | }), 13 | }, 14 | }; 15 | 16 | const adminAPIFailure = { 17 | "/admin/api/v2beta/": { 18 | status: 500, 19 | }, 20 | }; 21 | 22 | const adminAPISlow = { 23 | "/admin/api/v2beta/": 1000, 24 | }; 25 | 26 | module.exports = { 27 | wagtailUpgradeAvailable, 28 | adminAPIFailure, 29 | adminAPISlow, 30 | }; 31 | -------------------------------------------------------------------------------- /downloads-analysis/versions-share/version-share-over-time.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | DATE(downloads.timestamp) AS day, 3 | downloads.file.version AS version, 4 | COUNT(*) AS downloads_count 5 | FROM 6 | `bigquery-public-data.pypi.file_downloads` AS downloads 7 | WHERE 8 | downloads.project = 'wagtail' 9 | AND ( 10 | SUBSTRING(downloads.file.version,0,1) = '2' 11 | OR SUBSTRING(downloads.file.version,0,1) = '3' 12 | OR SUBSTRING(downloads.file.version,0,1) = '4' 13 | OR SUBSTRING(downloads.file.version,0,1) = '5' 14 | OR SUBSTRING(downloads.file.version,0,1) = '6' 15 | ) 16 | AND (DATE(downloads.timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 5 day) 17 | AND CURRENT_DATE()) 18 | GROUP BY 19 | day, 20 | version 21 | ORDER BY 22 | day, 23 | version desc 24 | -------------------------------------------------------------------------------- /downloads-analysis/installers-stats.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | -- BigQuery – downloads of the Wagtail project by installer 3 | -- Sample & docs: https://github.com/thibaudcolas/wagtail-tooling/tree/main/downloads-analysis 4 | WITH filtered_downloads AS ( 5 | SELECT 6 | timestamp, 7 | details.installer.name AS installer 8 | FROM `bigquery-public-data.pypi.file_downloads` 9 | -- Change this to analyze another project. BEWARE OF THE COSTS 10 | -- Without this line – query cost = 7.5 TB (USD40) 11 | WHERE project = 'wagtail' 12 | AND timestamp BETWEEN TIMESTAMP('2024-01-01T00:00:01.000Z') AND CURRENT_TIMESTAMP() 13 | ) 14 | SELECT 15 | TIMESTAMP_TRUNC(timestamp, MONTH) AS month, 16 | installer, 17 | COUNT(*) AS num_downloads 18 | FROM filtered_downloads 19 | GROUP BY month, installer 20 | ORDER BY month, num_downloads DESC; 21 | -------------------------------------------------------------------------------- /http-archive/gaad-2024/wagtail-lighthouse-scores-2024-only.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages – only ones present in 2024-04 data and not 2023-04. 3 | SELECT 4 | url, 5 | JSON_EXTRACT_SCALAR(report, '$.categories.accessibility.score') AS accessibility, 6 | JSON_EXTRACT_SCALAR(report, '$.categories.performance.score') AS performance, 7 | JSON_EXTRACT_SCALAR(report, '$.categories.seo.score') AS seo, 8 | JSON_EXTRACT_SCALAR(report, '$.categories.best-practices.score') AS best_practices, 9 | FROM 10 | `wagtail-analysis.wagtail_httparchive.2024_04_19_wagtail_crux_urls` AS four 11 | JOIN 12 | `wagtail-analysis.wagtail_httparchive.django-wagtail-lighthouse-2024_04_01_desktop` 13 | USING (url) 14 | WHERE NOT EXISTS ( 15 | SELECT 1 16 | FROM `wagtail_httparchive.wagtail-crux-2023_04_01` as three 17 | WHERE four.url = three.url 18 | ) 19 | -------------------------------------------------------------------------------- /http-archive/gaad-2025/wagtail-lighthouse-scores-2025-only.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages, 2025-04-01 data 3 | SELECT 4 | page, 5 | rank, 6 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.accessibility.score') AS accessibility, 7 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.performance.score') AS performance, 8 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.seo.score') AS seo, 9 | JSON_EXTRACT_SCALAR(lighthouse, '$.categories.best-practices.score') AS best_practices, 10 | FROM 11 | `wagtail-analysis.wagtail_httparchive.2025_04_01_django_wagtail_reports` as latest 12 | WHERE 13 | lighthouse IS NOT NULL 14 | AND EXISTS (SELECT 1 FROM UNNEST(technologies) AS tech WHERE tech.technology = 'Wagtail') 15 | AND NOT EXISTS (SELECT 1 FROM `wagtail-analysis.wagtail_httparchive.django-wagtail-lighthouse-2024_04_01_desktop` as prev WHERE prev.url = latest.page) 16 | ORDER BY rank ASC 17 | -------------------------------------------------------------------------------- /backstop/engine_scripts/playwright/interceptImages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * INTERCEPT IMAGES 3 | * Listen to all requests. If a request matches IMAGE_URL_RE 4 | * then stub the image with data from IMAGE_STUB_URL 5 | * 6 | * Use this in an onBefore script E.G. 7 | ``` 8 | module.exports = async function(page, scenario) { 9 | require('./interceptImages')(page, scenario); 10 | } 11 | ``` 12 | * 13 | */ 14 | 15 | const fs = require('fs'); 16 | const path = require('path'); 17 | 18 | const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i; 19 | const IMAGE_STUB_URL = path.resolve(__dirname, '../../imageStub.jpg'); 20 | const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL); 21 | const HEADERS_STUB = {}; 22 | 23 | module.exports = async function (page, scenario) { 24 | page.route(IMAGE_URL_RE, route => { 25 | route.fulfill({ 26 | body: IMAGE_DATA_BUFFER, 27 | headers: HEADERS_STUB, 28 | status: 200 29 | }); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /downloads-analysis/ci-installers-stats.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | -- BigQuery – downloads of the Wagtail project by installer, by "likely CI" 3 | -- Sample & docs: https://github.com/thibaudcolas/wagtail-tooling/tree/main/downloads-analysis 4 | WITH filtered_downloads AS ( 5 | SELECT 6 | timestamp, 7 | details.installer.name AS installer, 8 | details.ci AS ci 9 | FROM `bigquery-public-data.pypi.file_downloads` 10 | -- Change this to analyze another project. BEWARE OF THE COSTS 11 | -- Without this line – query cost = 7.5 TB (USD40) 12 | -- With this line - expet a query cost around 5 - 30GB (likely free, USD 0.15 if over allowance) 13 | WHERE project = 'wagtail' 14 | AND timestamp BETWEEN TIMESTAMP('2024-01-01T00:00:01.000Z') AND CURRENT_TIMESTAMP() 15 | ) 16 | SELECT 17 | TIMESTAMP_TRUNC(timestamp, MONTH) AS month, 18 | installer, 19 | ci, 20 | COUNT(*) AS num_downloads 21 | FROM filtered_downloads 22 | GROUP BY month, installer, ci 23 | ORDER BY month, num_downloads DESC, ci; 24 | -------------------------------------------------------------------------------- /http-archive/aria-label/README.md: -------------------------------------------------------------------------------- 1 | # aria-label analysis 2 | 3 | Looks for problem patterns in aria-label usage within HTML. Published post: [aria-label is a letdown](https://wagtail.org/blog/aria-label-is-a-letdown/). 4 | 5 | The primary dataset is homepages of Wagtail websites, as determined in the HTTP Archive. However both the methodology and the results are likely to be generally relevant for any technology. 6 | 7 | Raw data and analysis (Google Sheets): [aria-label problems in the wild - Wagtail websites](https://docs.google.com/spreadsheets/d/1tgTxF7fg_VlByCDIW2i-Ir-T8OvhD7sHZHjfCXg6nks/edit). 8 | 9 | | Pattern | % | Count | 10 | | --------------------------- | ---- | ----- | 11 | | Missing visible text | 21% | 322 | 12 | | Starts without visible text | 23% | 354 | 13 | | Disallowed for role | 4% | 64 | 14 | | Overrides visible text | 32% | 480 | 15 | | Any of those issues | 34% | 511 | 16 | | Total | 100% | 1517 | 17 | -------------------------------------------------------------------------------- /backstop/engine_scripts/playwright/loadCookies.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | module.exports = async (browserContext, scenario) => { 4 | let cookies = []; 5 | 6 | if (scenario.sessionid) { 7 | cookies.push({ 8 | domain: "localhost", 9 | path: "/", 10 | name: "sessionid", 11 | value: scenario.sessionid, 12 | expirationDate: new Date().getTime() / 1000 + 60 * 60 * 24 * 30, // 30 days 13 | hostOnly: false, 14 | httpOnly: false, 15 | secure: false, 16 | session: false, 17 | sameSite: "Lax", 18 | }); 19 | } 20 | 21 | if (scenario.auto_login) { 22 | cookies.push({ 23 | domain: "localhost", 24 | path: "/", 25 | name: "auto_login", 26 | value: scenario.auto_login, 27 | expirationDate: new Date().getTime() / 1000 + 60 * 60 * 24 * 30, // 30 days 28 | hostOnly: false, 29 | httpOnly: false, 30 | secure: false, 31 | session: false, 32 | sameSite: "Lax", 33 | }); 34 | } 35 | 36 | // Add cookies to browser 37 | browserContext.addCookies(cookies); 38 | }; 39 | -------------------------------------------------------------------------------- /ui-benchmark/.lighthouserc.js: -------------------------------------------------------------------------------- 1 | const origin = process.env.TEST_ORIGIN ?? "http://localhost:8000"; 2 | 3 | module.exports = { 4 | ci: { 5 | collect: { 6 | numberOfRuns: 1, 7 | url: [ 8 | // Dashboard 9 | `${origin}/admin/`, 10 | // Userbar 11 | `${origin}/blog/wild-yeast/`, 12 | // Pages edit 13 | `${origin}/admin/pages/81/edit/`, 14 | // Pages listing 15 | `${origin}/admin/pages/80/`, 16 | // Images listing 17 | `${origin}/admin/images/`, 18 | // Styleguide 19 | `${origin}/admin/styleguide/`, 20 | ], 21 | numberOfRuns: 3, 22 | }, 23 | assert: { 24 | assertions: { 25 | "categories:performance": ["error", { minScore: 0.8 }], 26 | "categories:accessibility": ["error", { minScore: 1 }], 27 | "categories:best-practices": ["error", { minScore: 1 }], 28 | "categories:seo": ["error", { minScore: 1 }], 29 | }, 30 | }, 31 | upload: { 32 | target: "filesystem", 33 | outputDir: "./reports", 34 | }, 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /http-archive/gaad-2024/wagtail-lighthouse-scores-2023-2024.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages, 2024-04-01 data, 2023-03-01 URLs 3 | SELECT 4 | three.url, 5 | JSON_EXTRACT_SCALAR(four.report, '$.categories.accessibility.score') AS accessibility_2024, 6 | JSON_EXTRACT_SCALAR(four.report, '$.categories.performance.score') AS performance_2024, 7 | JSON_EXTRACT_SCALAR(four.report, '$.categories.seo.score') AS SEO_2024, 8 | JSON_EXTRACT_SCALAR(four.report, '$.categories.best-practices.score') AS best_practices_2024, 9 | JSON_EXTRACT_SCALAR(three.report, '$.categories.accessibility.score') AS accessibility_2023, 10 | JSON_EXTRACT_SCALAR(three.report, '$.categories.performance.score') AS performance_2023, 11 | JSON_EXTRACT_SCALAR(three.report, '$.categories.seo.score') AS SEO_2023, 12 | JSON_EXTRACT_SCALAR(three.report, '$.categories.best-practices.score') AS best_practices_2023, 13 | FROM 14 | `wagtail_httparchive.wagtail-lighthouse-2023_04_01` as three, 15 | `wagtail-analysis.wagtail_httparchive.django-wagtail-lighthouse-2024_04_01_desktop` as four 16 | WHERE 17 | three.url = four.url 18 | -------------------------------------------------------------------------------- /http-archive/gaad-2025/wagtail-lighthouse-scores-2024-2025.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse scores for Wagtail sites’ homepages, 2024-04-01 data, 2023-03-01 URLs 3 | SELECT 4 | three.url, 5 | JSON_EXTRACT_SCALAR(four.report, '$.categories.accessibility.score') AS accessibility_2024, 6 | JSON_EXTRACT_SCALAR(four.report, '$.categories.performance.score') AS performance_2024, 7 | JSON_EXTRACT_SCALAR(four.report, '$.categories.seo.score') AS SEO_2024, 8 | JSON_EXTRACT_SCALAR(four.report, '$.categories.best-practices.score') AS best_practices_2024, 9 | JSON_EXTRACT_SCALAR(three.report, '$.categories.accessibility.score') AS accessibility_2023, 10 | JSON_EXTRACT_SCALAR(three.report, '$.categories.performance.score') AS performance_2023, 11 | JSON_EXTRACT_SCALAR(three.report, '$.categories.seo.score') AS SEO_2023, 12 | JSON_EXTRACT_SCALAR(three.report, '$.categories.best-practices.score') AS best_practices_2023, 13 | FROM 14 | `wagtail_httparchive.wagtail-lighthouse-2023_04_01` as three, 15 | `wagtail-analysis.wagtail_httparchive.django-wagtail-lighthouse-2024_04_01_desktop` as four 16 | WHERE 17 | three.url = four.url 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thibaud Colas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/interceptImages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * INTERCEPT IMAGES 3 | * Listen to all requests. If a request matches IMAGE_URL_RE 4 | * then stub the image with data from IMAGE_STUB_URL 5 | * 6 | * Use this in an onBefore script E.G. 7 | ``` 8 | module.exports = async function(page, scenario) { 9 | require('./interceptImages')(page, scenario); 10 | } 11 | ``` 12 | * 13 | */ 14 | 15 | const fs = require("fs"); 16 | const path = require("path"); 17 | 18 | const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i; 19 | const IMAGE_STUB_URL = path.resolve(__dirname, "../imageStub.jpg"); 20 | const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL); 21 | const HEADERS_STUB = {}; 22 | 23 | module.exports = async function (page, scenario) { 24 | const intercept = async (request, targetUrl) => { 25 | if (IMAGE_URL_RE.test(request.url())) { 26 | await request.respond({ 27 | body: IMAGE_DATA_BUFFER, 28 | headers: HEADERS_STUB, 29 | status: 200, 30 | }); 31 | } else { 32 | request.continue(); 33 | } 34 | }; 35 | await page.setRequestInterception(true); 36 | page.on("request", intercept); 37 | }; 38 | -------------------------------------------------------------------------------- /http-archive/gaad-2025/README.md: -------------------------------------------------------------------------------- 1 | # GAAD 2025 2 | 3 | ```sql 4 | SELECT * FROM `httparchive.crawl.pages` TABLESAMPLE SYSTEM (0.0001 PERCENT) WHERE date = "2025-04-01" 5 | ``` 6 | 7 | ## References 8 | 9 | - 2025 spreadsheet: [Wagtail sites accessibility GAAD 2025](https://docs.google.com/spreadsheets/d/18tCgJWHodj5a8Pfe_m63E5PvxUwIXQuY30rUbizGtIU/edit) 10 | - 2024 spreadsheet: [Wagtail sites accessibility GAAD 2024](https://docs.google.com/spreadsheets/d/1hQXCSbvAtmdf7IArBT4RL3cvgldCUx1UPGzABC_g8Dc/edit) 11 | - 2023 spreadsheet: [Wagtail sites accessibility GAAD 2023](https://docs.google.com/spreadsheets/d/1dLpW6fbcl-AsVQNVhihzi1p-fY5gByZK_EQWt-EtCoM/edit) 12 | 13 | ### Data sources 14 | 15 | All data is fetched from the HTTP Archive via [Google BigQuery](https://cloud.google.com/bigquery/). For more information, see their [Getting started accessing the HTTP Archive with BigQuery](https://har.fyi/guides/getting-started/). 16 | 17 | - [Chrome UX report](https://developer.chrome.com/docs/crux/) websites dataset 18 | - [HTTP Archive Web Almanac](https://almanac.httparchive.org/) analysis techniques and queries 19 | - Wappalyzer technology detection ([2023 HTTP Archive fork](https://github.com/HTTPArchive/wappalyzer)) 20 | -------------------------------------------------------------------------------- /ui-benchmark/metrics.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for file in *.json; do 4 | # Extract the metrics using jq 5 | performance=$(jq '.categories.performance.score' "$file") 6 | fcp=$(jq '.audits["first-contentful-paint"].numericValue' "$file") 7 | lcp=$(jq '.audits["largest-contentful-paint"].numericValue' "$file") 8 | tbt=$(jq '.audits["total-blocking-time"].numericValue' "$file") 9 | cls=$(jq '.audits["cumulative-layout-shift"].numericValue' "$file") 10 | speed_index=$(jq '.audits["speed-index"].numericValue' "$file") 11 | # accessibility=$(jq '.categories.accessibility.score' "$file") 12 | # best_practices=$(jq '.categories["best-practices"].score' "$file") 13 | # seo=$(jq '.categories.seo.score' "$file") 14 | tti=$(jq '.audits["interactive"].numericValue' "$file") 15 | fid=$(jq '.audits["max-potential-fid"].numericValue' "$file") 16 | 17 | echo "File: $file" 18 | echo "Performance: $performance" 19 | echo "FCP: $fcp" 20 | echo "LCP: $lcp" 21 | echo "TBT: $tbt" 22 | echo "CLS: $cls" 23 | echo "Speed Index: $speed_index" 24 | # echo "Accessibility: $accessibility" 25 | # echo "Best Practices: $best_practices" 26 | # echo "SEO: $seo" 27 | echo "TTI: $tti" 28 | echo "Max Potential FID: $fid" 29 | done 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wagtail-tooling", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "", 6 | "main": "backstop.js", 7 | "keywords": [], 8 | "author": "Thibaud Colas", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "backstopjs": "^6.3.25", 12 | "convert-array-to-csv": "^2.0.0", 13 | "dotenv": "^16.4.5" 14 | }, 15 | "scripts": { 16 | "// Base backstop.js visual regression tests": "", 17 | "backstop": "backstop --config=backstop/backstop.config.js", 18 | "regression:test": "npm run backstop -s -- test", 19 | "regression:approve": "npm run backstop -s -- approve", 20 | "regression:open": "npm run backstop -s -- openReport", 21 | "// Base Pa11y accessibility tests": "", 22 | "pa11y": "node accessibility/pa11y-test.js", 23 | "// Contrast themes tests": "", 24 | "contrast:backstop": "backstop --config=contrast-themes/backstop.config.js", 25 | "contrast:regression:test": "npm run contrast:backstop -s -- test", 26 | "contrast:regression:approve": "npm run contrast:backstop -s -- approve", 27 | "contrast:regression:open": "npm run contrast:backstop -s -- openReport", 28 | "// docs screenshots": "", 29 | "docs:backstop": "backstop --config=docs-screenshots/backstop.config.js", 30 | "docs:regression:test": "npm run docs:backstop -s -- test", 31 | "docs:regression:approve": "npm run docs:backstop -s -- approve", 32 | "docs:regression:open": "npm run docs:backstop -s -- openReport" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /accessibility/docs/axe-htmlcs-mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "area-alt": "", 3 | "aria-allowed-attr": "", 4 | "aria-dpub-role-fallback": "", 5 | "aria-hidden-body": "", 6 | "aria-hidden-focus": "", 7 | "aria-required-attr": "", 8 | "aria-required-children": "", 9 | "aria-required-parent": "", 10 | "aria-roles": "", 11 | "aria-valid-attr-value": "", 12 | "aria-valid-attr": "", 13 | "audio-caption": "", 14 | "blink": "", 15 | "button-name": "", 16 | "bypass": "", 17 | "color-contrast": "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail", 18 | "definition-list": "", 19 | "dlitem": "", 20 | "document-title": "", 21 | "duplicate-id-active": "", 22 | "duplicate-id-aria": "", 23 | "duplicate-id": "", 24 | "form-field-multiple-labels": "", 25 | "frame-title": "", 26 | "html-has-lang": "", 27 | "html-lang-valid": "", 28 | "html-xml-lang-mismatch": "", 29 | "image-alt": "WCAG2AA.Principle1.Guideline1_1.1_1_1.H37", 30 | "input-image-alt": "", 31 | "label": "WCAG2AA.Principle1.Guideline1_3.1_3_1.F68", 32 | "layout-table": "", 33 | "link-in-text-block": "", 34 | "link-name": "", 35 | "list": "", 36 | "listitem": "", 37 | "marquee": "", 38 | "meta-refresh": "", 39 | "meta-viewport": "", 40 | "object-alt": "", 41 | "p-as-heading": "", 42 | "server-side-image-map": "", 43 | "table-fake-caption": "", 44 | "td-has-header": "", 45 | "td-headers-attr": "", 46 | "th-has-data-cells": "", 47 | "valid-lang": "", 48 | "video-caption": "", 49 | "video-description": "" 50 | } 51 | -------------------------------------------------------------------------------- /accessibility/pa11y-report.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { convertArrayToCSV } = require("convert-array-to-csv"); 3 | 4 | const { getUniqueIssues } = require("./pa11y-dedupe"); 5 | 6 | const csvHeader = [ 7 | "Issue", 8 | "Code", 9 | "Standard", 10 | "Level", 11 | "Success criteria", 12 | "Impact", 13 | "Occurences", 14 | "Selector", 15 | "Context", 16 | "Occurences", 17 | "Screenshots", 18 | ]; 19 | 20 | const uniqueIssues = getUniqueIssues(); 21 | 22 | const rows = uniqueIssues.map((issue) => { 23 | const { 24 | label, 25 | code, 26 | wcagSC, 27 | standard, 28 | wcagLevel, 29 | impact, 30 | context, 31 | selector, 32 | instances, 33 | } = issue; 34 | 35 | const instancesLabel = issue.instances 36 | .map( 37 | (instance) => 38 | `${instance.label} (${instance.pageUrl.replace( 39 | "http://localhost:8000/admin", 40 | "", 41 | )})`, 42 | ) 43 | .join(", ") 44 | .substr(0, 100); 45 | 46 | const screenshots = issue.instances 47 | .map((instance) => instance.screenshot) 48 | .join(", ") 49 | .substr(0, 100); 50 | 51 | return [ 52 | label, 53 | code, 54 | standard, 55 | wcagLevel, 56 | wcagSC, 57 | impact, 58 | instances.length, 59 | selector, 60 | context, 61 | instancesLabel, 62 | screenshots, 63 | ]; 64 | }); 65 | 66 | const csv = convertArrayToCSV(rows, { 67 | header: csvHeader, 68 | }); 69 | 70 | fs.writeFileSync("pa11y.csv", csv, "utf8"); 71 | -------------------------------------------------------------------------------- /http-archive/sustainability/wagtail-crux-weights-2025.sql: -------------------------------------------------------------------------------- 1 | -- Inspired by HTTP Archive Web Almanac 2024 page weight reports. 2 | -- See https://github.com/HTTPArchive/almanac.httparchive.org/blob/main/sql/2024/page-weight/bytes_per_type.sql 3 | SELECT 4 | page, 5 | rank, 6 | ROUND(CAST(JSON_VALUE(summary, '$.bytesTotal') AS INT64) / 1024, 2) AS total_kbytes, 7 | ROUND(CAST(JSON_VALUE(summary, '$.bytesHtml') AS INT64) / 1024, 2) AS html_kbytes, 8 | ROUND(CAST(JSON_VALUE(summary, '$.bytesJS') AS INT64) / 1024, 2) AS js_kbytes, 9 | ROUND(CAST(JSON_VALUE(summary, '$.bytesCss') AS INT64) / 1024, 2) AS css_kbytes, 10 | ROUND(CAST(JSON_VALUE(summary, '$.bytesImg') AS INT64) / 1024, 2) AS img_kbytes, 11 | ROUND(CAST(JSON_VALUE(summary, '$.bytesGif') AS INT64) / 1024, 2) AS gif_kbytes, 12 | ROUND(CAST(JSON_VALUE(summary, '$.bytesPng') AS INT64) / 1024, 2) AS png_kbytes, 13 | ROUND(CAST(JSON_VALUE(summary, '$.bytesJpg') AS INT64) / 1024, 2) AS jpg_kbytes, 14 | ROUND(CAST(JSON_VALUE(summary, '$.bytesWebp') AS INT64) / 1024, 2) AS webp_kbytes, 15 | ROUND(CAST(JSON_VALUE(summary, '$.bytesSvg') AS INT64) / 1024, 2) AS svg_kbytes, 16 | ROUND(CAST(JSON_VALUE(summary, '$.bytesVideo') AS INT64) / 1024, 2) AS video_kbytes, 17 | ROUND(CAST(JSON_VALUE(summary, '$.bytesFont') AS INT64) / 1024, 2) AS font_kbytes 18 | FROM 19 | `wagtail_httparchive.2025_04_01_django_wagtail_reports` 20 | WHERE 21 | CAST(JSON_VALUE(summary, '$.bytesTotal') AS INT64) > 0 22 | AND EXISTS (SELECT 1 FROM UNNEST(technologies) AS tech WHERE tech.technology = 'Wagtail') 23 | ORDER BY 24 | rank 25 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/loadCookies.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | module.exports = async (page, scenario) => { 4 | let cookies = []; 5 | 6 | if (scenario.sessionid) { 7 | cookies.push({ 8 | domain: "localhost", 9 | path: "/", 10 | name: "sessionid", 11 | value: scenario.sessionid, 12 | expirationDate: new Date().getTime() / 1000 + 60 * 60 * 24 * 30, // 30 days 13 | hostOnly: false, 14 | httpOnly: false, 15 | secure: false, 16 | session: false, 17 | sameSite: "Lax", 18 | }); 19 | } 20 | 21 | if (scenario.auto_login) { 22 | cookies.push({ 23 | domain: "localhost", 24 | path: "/", 25 | name: "auto_login", 26 | value: scenario.auto_login, 27 | expirationDate: new Date().getTime() / 1000 + 60 * 60 * 24 * 30, // 30 days 28 | hostOnly: false, 29 | httpOnly: false, 30 | secure: false, 31 | session: false, 32 | sameSite: "Lax", 33 | }); 34 | } 35 | 36 | // MUNGE COOKIE DOMAIN 37 | cookies = cookies.map((cookie) => { 38 | if ( 39 | cookie.domain.startsWith("http://") || 40 | cookie.domain.startsWith("https://") 41 | ) { 42 | cookie.url = cookie.domain; 43 | } else { 44 | cookie.url = "https://" + cookie.domain; 45 | } 46 | delete cookie.domain; 47 | return cookie; 48 | }); 49 | 50 | // SET COOKIES 51 | const setCookies = async () => { 52 | return Promise.all( 53 | cookies.map(async (cookie) => { 54 | await page.setCookie(cookie); 55 | }), 56 | ); 57 | }; 58 | await setCookies(); 59 | }; 60 | -------------------------------------------------------------------------------- /http-archive/gaad-2023/LH audit scores 2023-04-01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse audit scores for Django projects 3 | # Note scores, weightings, groups and descriptions may be off in mixed months when new versions of Lighthouse roles out 4 | 5 | CREATE TEMPORARY FUNCTION getAudits(report STRING, category STRING) 6 | RETURNS ARRAY> LANGUAGE js AS ''' 7 | var $ = JSON.parse(report); 8 | var auditrefs = $.categories[category].auditRefs; 9 | var audits = $.audits; 10 | $ = null; 11 | var results = []; 12 | for (auditref of auditrefs) { 13 | results.push({ 14 | id: auditref.id, 15 | weight: auditref.weight, 16 | audit_group: auditref.group, 17 | description: audits[auditref.id].description, 18 | score: audits[auditref.id].score 19 | }); 20 | } 21 | return results; 22 | '''; 23 | 24 | SELECT 25 | audits.id AS id, 26 | COUNTIF(audits.score > 0) AS num_pages, 27 | COUNT(0) AS total, 28 | COUNTIF(audits.score IS NOT NULL) AS total_applicable, 29 | SAFE_DIVIDE(COUNTIF(audits.score > 0), COUNTIF(audits.score IS NOT NULL)) AS pct, 30 | APPROX_QUANTILES(audits.weight, 100)[OFFSET(50)] AS median_weight, 31 | MAX(audits.audit_group) AS audit_group, 32 | MAX(audits.description) AS description 33 | FROM 34 | `httparchive.lighthouse.2023_04_01_desktop`, 35 | UNNEST(getAudits(report, 'accessibility')) AS audits 36 | WHERE 37 | LENGTH(report) < 20000000 # necessary to avoid out of memory issues. Excludes very large results 38 | GROUP BY 39 | audits.id 40 | ORDER BY 41 | median_weight DESC, 42 | id 43 | -------------------------------------------------------------------------------- /http-archive/gaad-2023/Django LH audit scores 2023-04-01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse audit scores for Django projects 3 | # Note scores, weightings, groups and descriptions may be off in mixed months when new versions of Lighthouse roles out 4 | 5 | CREATE TEMPORARY FUNCTION getAudits(report STRING, category STRING) 6 | RETURNS ARRAY> LANGUAGE js AS ''' 7 | var $ = JSON.parse(report); 8 | var auditrefs = $.categories[category].auditRefs; 9 | var audits = $.audits; 10 | $ = null; 11 | var results = []; 12 | for (auditref of auditrefs) { 13 | results.push({ 14 | id: auditref.id, 15 | weight: auditref.weight, 16 | audit_group: auditref.group, 17 | description: audits[auditref.id].description, 18 | score: audits[auditref.id].score 19 | }); 20 | } 21 | return results; 22 | '''; 23 | 24 | SELECT 25 | audits.id AS id, 26 | COUNTIF(audits.score > 0) AS num_pages, 27 | COUNT(0) AS total, 28 | COUNTIF(audits.score IS NOT NULL) AS total_applicable, 29 | SAFE_DIVIDE(COUNTIF(audits.score > 0), COUNTIF(audits.score IS NOT NULL)) AS pct, 30 | APPROX_QUANTILES(audits.weight, 100)[OFFSET(50)] AS median_weight, 31 | MAX(audits.audit_group) AS audit_group, 32 | MAX(audits.description) AS description 33 | FROM 34 | `wagtail_httparchive.django-lighthouse-2023_04_01_desktop`, 35 | UNNEST(getAudits(report, 'accessibility')) AS audits 36 | WHERE 37 | LENGTH(report) < 20000000 # necessary to avoid out of memory issues. Excludes very large results 38 | GROUP BY 39 | audits.id 40 | ORDER BY 41 | median_weight DESC, 42 | id 43 | -------------------------------------------------------------------------------- /http-archive/gaad-2023/Wagtail LH audit scores 2023_04_01_desktop.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse audit scores for Wagtail projects 3 | # Note scores, weightings, groups and descriptions may be off in mixed months when new versions of Lighthouse roles out 4 | 5 | CREATE TEMPORARY FUNCTION getAudits(report STRING, category STRING) 6 | RETURNS ARRAY> LANGUAGE js AS ''' 7 | var $ = JSON.parse(report); 8 | var auditrefs = $.categories[category].auditRefs; 9 | var audits = $.audits; 10 | $ = null; 11 | var results = []; 12 | for (auditref of auditrefs) { 13 | results.push({ 14 | id: auditref.id, 15 | weight: auditref.weight, 16 | audit_group: auditref.group, 17 | description: audits[auditref.id].description, 18 | score: audits[auditref.id].score 19 | }); 20 | } 21 | return results; 22 | '''; 23 | 24 | SELECT 25 | audits.id AS id, 26 | COUNTIF(audits.score > 0) AS num_pages, 27 | COUNT(0) AS total, 28 | COUNTIF(audits.score IS NOT NULL) AS total_applicable, 29 | SAFE_DIVIDE(COUNTIF(audits.score > 0), COUNTIF(audits.score IS NOT NULL)) AS pct, 30 | APPROX_QUANTILES(audits.weight, 100)[OFFSET(50)] AS median_weight, 31 | MAX(audits.audit_group) AS audit_group, 32 | MAX(audits.description) AS description 33 | FROM 34 | `wagtail_httparchive.wagtail-lighthouse-2023_04_01`, 35 | UNNEST(getAudits(report, 'accessibility')) AS audits 36 | WHERE 37 | LENGTH(report) < 20000000 # necessary to avoid out of memory issues. Excludes very large results 38 | GROUP BY 39 | audits.id 40 | ORDER BY 41 | median_weight DESC, 42 | id 43 | -------------------------------------------------------------------------------- /http-archive/gaad-2023/Django LH audit scores 2023_08_01_desktop.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse audit scores for Wagtail projects 3 | # Note scores, weightings, groups and descriptions may be off in mixed months when new versions of Lighthouse roles out 4 | 5 | CREATE TEMPORARY FUNCTION getAudits(report STRING, category STRING) 6 | RETURNS ARRAY> LANGUAGE js AS ''' 7 | var $ = JSON.parse(report); 8 | var auditrefs = $.categories[category].auditRefs; 9 | var audits = $.audits; 10 | $ = null; 11 | var results = []; 12 | for (auditref of auditrefs) { 13 | results.push({ 14 | id: auditref.id, 15 | weight: auditref.weight, 16 | audit_group: auditref.group, 17 | description: audits[auditref.id].description, 18 | score: audits[auditref.id].score 19 | }); 20 | } 21 | return results; 22 | '''; 23 | 24 | SELECT 25 | audits.id AS id, 26 | COUNTIF(audits.score > 0) AS num_pages, 27 | COUNT(0) AS total, 28 | COUNTIF(audits.score IS NOT NULL) AS total_applicable, 29 | SAFE_DIVIDE(COUNTIF(audits.score > 0), COUNTIF(audits.score IS NOT NULL)) AS pct, 30 | APPROX_QUANTILES(audits.weight, 100)[OFFSET(50)] AS median_weight, 31 | MAX(audits.audit_group) AS audit_group, 32 | MAX(audits.description) AS description 33 | FROM 34 | `wagtail_httparchive.django-wagtail-lighthouse-2023_08_01_desktop`, 35 | UNNEST(getAudits(report, 'accessibility')) AS audits 36 | WHERE 37 | LENGTH(report) < 20000000 # necessary to avoid out of memory issues. Excludes very large results 38 | and app = 'Django' 39 | GROUP BY 40 | audits.id 41 | ORDER BY 42 | median_weight DESC, 43 | id 44 | -------------------------------------------------------------------------------- /http-archive/legacy/Wagtail LH audit scores 2023_08_01_desktop.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse audit scores for Wagtail projects 3 | # Note scores, weightings, groups and descriptions may be off in mixed months when new versions of Lighthouse roles out 4 | 5 | CREATE TEMPORARY FUNCTION getAudits(report STRING, category STRING) 6 | RETURNS ARRAY> LANGUAGE js AS ''' 7 | var $ = JSON.parse(report); 8 | var auditrefs = $.categories[category].auditRefs; 9 | var audits = $.audits; 10 | $ = null; 11 | var results = []; 12 | for (auditref of auditrefs) { 13 | results.push({ 14 | id: auditref.id, 15 | weight: auditref.weight, 16 | audit_group: auditref.group, 17 | description: audits[auditref.id].description, 18 | score: audits[auditref.id].score 19 | }); 20 | } 21 | return results; 22 | '''; 23 | 24 | SELECT 25 | audits.id AS id, 26 | COUNTIF(audits.score > 0) AS num_pages, 27 | COUNT(0) AS total, 28 | COUNTIF(audits.score IS NOT NULL) AS total_applicable, 29 | SAFE_DIVIDE(COUNTIF(audits.score > 0), COUNTIF(audits.score IS NOT NULL)) AS pct, 30 | APPROX_QUANTILES(audits.weight, 100)[OFFSET(50)] AS median_weight, 31 | MAX(audits.audit_group) AS audit_group, 32 | MAX(audits.description) AS description 33 | FROM 34 | `wagtail_httparchive.django-wagtail-lighthouse-2023_08_01_desktop`, 35 | UNNEST(getAudits(report, 'accessibility')) AS audits 36 | WHERE 37 | LENGTH(report) < 20000000 # necessary to avoid out of memory issues. Excludes very large results 38 | and app = 'Wagtail' 39 | GROUP BY 40 | audits.id 41 | ORDER BY 42 | median_weight DESC, 43 | id 44 | -------------------------------------------------------------------------------- /backstop/engine_scripts/playwright/clickAndHoverHelper.js: -------------------------------------------------------------------------------- 1 | module.exports = async (page, scenario) => { 2 | const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector; 3 | const clickSelector = scenario.clickSelectors || scenario.clickSelector; 4 | const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector; 5 | const scrollToSelector = scenario.scrollToSelector; 6 | const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int] 7 | 8 | if (keyPressSelector) { 9 | for (const keyPressSelectorItem of [].concat(keyPressSelector)) { 10 | await page.waitForSelector(keyPressSelectorItem.selector); 11 | await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress); 12 | } 13 | } 14 | 15 | if (hoverSelector) { 16 | for (const hoverSelectorIndex of [].concat(hoverSelector)) { 17 | await page.waitForSelector(hoverSelectorIndex); 18 | await page.hover(hoverSelectorIndex); 19 | } 20 | } 21 | 22 | if (clickSelector) { 23 | for (const clickSelectorIndex of [].concat(clickSelector)) { 24 | await page.waitForSelector(clickSelectorIndex); 25 | await page.click(clickSelectorIndex); 26 | } 27 | } 28 | 29 | if (postInteractionWait) { 30 | if (parseInt(postInteractionWait) > 0) { 31 | await page.waitForTimeout(postInteractionWait); 32 | } else { 33 | await page.waitForSelector(postInteractionWait); 34 | } 35 | } 36 | 37 | if (scrollToSelector) { 38 | await page.waitForSelector(scrollToSelector); 39 | await page.evaluate(scrollToSelector => { 40 | document.querySelector(scrollToSelector).scrollIntoView(); 41 | }, scrollToSelector); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /http-archive/gaad-2025/lh-audit-scores.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | -- From https://github.com/HTTPArchive/almanac.httparchive.org/blob/main/sql/2024/accessibility/lighthouse_a11y_audits.sql 3 | # Get summary of all Lighthouse scores for a category 4 | CREATE TEMPORARY FUNCTION getAudits(report JSON, category STRING) 5 | RETURNS ARRAY> LANGUAGE js AS ''' 6 | try { 7 | var $ = report; 8 | var auditrefs = $.categories[category].auditRefs; 9 | var audits = $.audits; 10 | $ = null; 11 | var results = []; 12 | for (auditref of auditrefs) { 13 | results.push({ 14 | id: auditref.id, 15 | weight: auditref.weight, 16 | audit_group: auditref.group, 17 | description: audits[auditref.id].description, 18 | score: audits[auditref.id].score 19 | }); 20 | } 21 | return results; 22 | } catch (e) { 23 | return [{}]; 24 | } 25 | '''; 26 | SELECT 27 | audits.id AS id, 28 | COUNTIF(audits.score > 0) AS num_pages, 29 | COUNT(0) AS total, 30 | COUNTIF(audits.score IS NOT NULL) AS total_applicable, 31 | SAFE_DIVIDE(COUNTIF(audits.score > 0), COUNTIF(audits.score IS NOT NULL)) AS pct, 32 | APPROX_QUANTILES(audits.weight, 100)[OFFSET(50)] AS median_weight, 33 | MAX(audits.audit_group) AS audit_group, 34 | MAX(audits.description) AS description 35 | FROM 36 | `wagtail-analysis.wagtail_httparchive.2025_04_01_django_wagtail_reports`, 37 | UNNEST(getAudits(lighthouse, 'accessibility')) AS audits 38 | WHERE 39 | lighthouse IS NOT NULL 40 | AND EXISTS ( 41 | SELECT 1 FROM UNNEST(technologies) AS tech 42 | WHERE tech.technology = 'Wagtail' 43 | ) 44 | GROUP BY 45 | audits.id 46 | ORDER BY 47 | median_weight DESC, 48 | audits.id; 49 | -------------------------------------------------------------------------------- /http-archive/legacy/Wagtail LH perf audit scores 2024_04_01_desktop.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse audit scores for Wagtail projects 3 | # Note scores, weightings, groups and descriptions may be off in mixed months when new versions of Lighthouse roles out 4 | 5 | CREATE TEMPORARY FUNCTION getAudits(report STRING, category STRING) 6 | RETURNS ARRAY> LANGUAGE js AS ''' 7 | var $ = JSON.parse(report); 8 | var auditrefs = $.categories[category].auditRefs; 9 | var audits = $.audits; 10 | $ = null; 11 | var results = []; 12 | for (auditref of auditrefs) { 13 | results.push({ 14 | id: auditref.id, 15 | weight: auditref.weight, 16 | audit_group: auditref.group, 17 | description: audits[auditref.id].description, 18 | score: audits[auditref.id].score 19 | }); 20 | } 21 | return results; 22 | '''; 23 | 24 | SELECT 25 | audits.id AS id, 26 | COUNTIF(audits.score > 0) AS num_pages, 27 | COUNT(0) AS total, 28 | COUNTIF(audits.score IS NOT NULL) AS total_applicable, 29 | SAFE_DIVIDE(COUNTIF(audits.score > 0), COUNTIF(audits.score IS NOT NULL)) AS pct, 30 | APPROX_QUANTILES(audits.weight, 100)[OFFSET(50)] AS median_weight, 31 | MAX(audits.audit_group) AS audit_group, 32 | MAX(audits.description) AS description 33 | FROM 34 | `wagtail-analysis.wagtail_httparchive.2024_04_19_wagtail_crux_urls` 35 | JOIN 36 | `wagtail-analysis.wagtail_httparchive.django-wagtail-lighthouse-2024_04_01_desktop` 37 | USING (url), 38 | UNNEST(getAudits(report, 'performance')) AS audits 39 | WHERE 40 | LENGTH(report) < 20000000 # necessary to avoid out of memory issues. Excludes very large results 41 | GROUP BY 42 | audits.id 43 | ORDER BY 44 | median_weight DESC, 45 | id 46 | -------------------------------------------------------------------------------- /downloads-analysis/release-candidate-downloads.csv: -------------------------------------------------------------------------------- 1 | version,downloads_count 2 | 7.2rc1,594 3 | 7.1.2,31799 4 | 7.1.1,30309 5 | 7.1,2892 6 | 7.1rc1,323 7 | 7.0.2,34000 8 | 7.0.1,11430 9 | 7.0,9373 10 | 7.0rc1,674 11 | 6.4.1,30152 12 | 6.4,2946 13 | 6.4rc1,413 14 | 6.3.3,336 15 | 6.3.2,40498 16 | 6.3.1,13424 17 | 6.3,5498 18 | 6.3rc2,362 19 | 6.3rc1,229 20 | 6.2.3,63 21 | 6.2.2,28444 22 | 6.2.1,4844 23 | 6.2,2059 24 | 6.2rc1,626 25 | 6.1.3,28774 26 | 6.1.2,5040 27 | 6.1.1,5422 28 | 6.1,1715 29 | 6.1rc2,383 30 | 6.1rc1,580 31 | 6.0.3,96 32 | 6.0.2,18688 33 | 6.0.1,5682 34 | 6.0,881 35 | 6.0rc1,522 36 | 5.2.3,31344 37 | 5.2.2,10851 38 | 5.2.1,5646 39 | 5.2,6980 40 | 5.2rc1,1298 41 | 5.1.3,23063 42 | 5.1.2,5988 43 | 5.1.1,5569 44 | 5.1,1238 45 | 5.1rc1,1298 46 | 5.0.2,21456 47 | 5.0.1,2873 48 | 5.0,2099 49 | 5.0rc1,511 50 | 4.2.3,162 51 | 4.2.2,16366 52 | 4.2.1,1253 53 | 4.2,2785 54 | 4.2rc1,1122 55 | 4.1.2,213 56 | 4.1.1,25036 57 | 4.1,1284 58 | 4.1rc1,577 59 | 4.0.4,12450 60 | 4.0.3,439 61 | 4.0.2,4274 62 | 4.0.1,1717 63 | 4.0,2061 64 | 4.0rc2,751 65 | 4.0rc1,420 66 | 3.0.2,633 67 | 3.0.1,19343 68 | 3.0,2743 69 | 3.0rc3,852 70 | 3.0rc2,574 71 | 3.0rc1,632 72 | 2.16.2,27834 73 | 2.16.1,11209 74 | 2.16,864 75 | 2.16rc2,494 76 | 2.16rc1,478 77 | 2.15.3,9002 78 | 2.15.2,6737 79 | 2.15.1,10849 80 | 2.15,1376 81 | 2.15rc2,320 82 | 2.15rc1,409 83 | 2.14.2,14379 84 | 2.14.1,10232 85 | 2.14,1325 86 | 2.14rc1,581 87 | 2.13.4,15402 88 | 2.13.3,3441 89 | 2.13.2,5997 90 | 2.13.1,2324 91 | 2.13,1546 92 | 2.13rc3,809 93 | 2.13rc2,407 94 | 2.13rc1,637 95 | 2.12.4,20446 96 | 2.12.3,18527 97 | 2.12.2,1401 98 | 2.12.1,779 99 | 2.12,1619 100 | 2.12rc1,396 101 | 2.11.3,24645 102 | 2.11.2,2662 103 | 2.11.1,1334 104 | 2.11,1422 105 | -------------------------------------------------------------------------------- /http-archive/legacy/LH audit scores per URL Django-Wagtail 2024-04-01.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | # Lighthouse audit scores for Wagtail projects 3 | # Note scores, weightings, groups and descriptions may be off in mixed months when new versions of Lighthouse roles out 4 | 5 | CREATE TEMPORARY FUNCTION getAudits(report STRING) 6 | RETURNS ARRAY> LANGUAGE js AS ''' 7 | var $ = JSON.parse(report); 8 | var audits = $.audits; 9 | var accessibility = $.categories.accessibility 10 | var performance = $.categories.performance 11 | var seo = $.categories.seo 12 | var best = $.categories['best-practices'] 13 | $ = null; 14 | var results = []; 15 | for (auditref of accessibility.auditRefs) { 16 | results.push({ 17 | id: auditref.id, 18 | audit_group: auditref.group, 19 | score: audits[auditref.id].score 20 | }); 21 | } 22 | for (auditref of performance.auditRefs) { 23 | results.push({ 24 | id: auditref.id, 25 | audit_group: auditref.group, 26 | score: audits[auditref.id].score 27 | }); 28 | } 29 | for (auditref of seo.auditRefs) { 30 | results.push({ 31 | id: auditref.id, 32 | audit_group: auditref.group, 33 | score: audits[auditref.id].score 34 | }); 35 | } 36 | for (auditref of best.auditRefs) { 37 | results.push({ 38 | id: auditref.id, 39 | audit_group: auditref.group, 40 | score: audits[auditref.id].score 41 | }); 42 | } 43 | return results; 44 | '''; 45 | 46 | SELECT 47 | url, 48 | audits.id AS id, 49 | audits.audit_group as audit_group, 50 | audits.score as score 51 | FROM `wagtail-analysis.wagtail_httparchive.django-wagtail-lighthouse-2024_04_01_desktop` 52 | CROSS JOIN UNNEST(getAudits(report)) AS audits 53 | WHERE 54 | LENGTH(report) < 20000000 # necessary to avoid out of memory issues. Excludes very large results 55 | -------------------------------------------------------------------------------- /ui/export-figma.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const scenarios = require("./scenarios"); 3 | const allComponents = require("./components"); 4 | 5 | const { convertArrayToCSV } = require("convert-array-to-csv"); 6 | 7 | const rows = []; 8 | let lastCategory = ""; 9 | 10 | scenarios 11 | .filter((s) => !s.skipReport) 12 | .forEach((scenario, i) => { 13 | const isNewCategory = scenario.category !== lastCategory; 14 | 15 | if (isNewCategory) { 16 | rows.push([scenario.category]); 17 | lastCategory = scenario.category; 18 | } 19 | 20 | const components = scenario.components || []; 21 | const componentsLabel = components.map((c) => c.label || c).join(", "); 22 | const componentsWorkNeeded = Array.from( 23 | new Set( 24 | components.map((component) => { 25 | const comp = allComponents.find( 26 | (c) => c.label === component.label || c.label === component, 27 | ); 28 | return comp?.workNeeded || null; 29 | }), 30 | ), 31 | ) 32 | .filter(Boolean) 33 | .join(", "); 34 | const workNeededLabel = scenario.workNeeded || componentsWorkNeeded; 35 | const meta = `${scenario.nextReleaseTarget}${ 36 | workNeededLabel ? ": " : "" 37 | }${workNeededLabel}`; 38 | 39 | rows.push([ 40 | "", 41 | scenario.label, 42 | componentsLabel, 43 | `https://bakerydemo-thibaudcolas6.herokuapp.com/admin${scenario.path}`, 44 | meta, 45 | ]); 46 | 47 | const states = scenario.states || []; 48 | 49 | states.forEach((s) => { 50 | if (s.path && s.path !== scenario.path) { 51 | rows.push([ 52 | "", 53 | s.label, 54 | "", 55 | `https://bakerydemo-thibaudcolas6.herokuapp.com/admin${s.path}`, 56 | ]); 57 | } 58 | }); 59 | }); 60 | 61 | const csv = convertArrayToCSV(rows, { 62 | // prettier-ignore 63 | header: [ 64 | "Category", 65 | "View", 66 | "Components", 67 | "URL", 68 | "Meta", 69 | ], 70 | }); 71 | 72 | fs.writeFileSync(`./ui/data/figma.csv`, csv, "utf8"); 73 | -------------------------------------------------------------------------------- /accessibility/old/axe.puppeteer.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require("puppeteer"); 2 | const fs = require("fs"); 3 | 4 | const scenarios = require("../ui/scenarios"); 5 | 6 | const views = {}; 7 | 8 | scenarios.forEach((scenario) => { 9 | if (!views[scenario.path]) { 10 | views[scenario.path] = `${scenario.category} – ${scenario.label}`; 11 | } 12 | }); 13 | 14 | const run = async () => { 15 | const browser = await puppeteer.launch(); 16 | let issues = []; 17 | 18 | for (const path of Object.keys(views)) { 19 | try { 20 | const label = views[path]; 21 | console.log(path); 22 | const page = await browser.newPage(); 23 | await page.setCookie({ 24 | domain: "localhost", 25 | path: "/", 26 | name: "sessionid", 27 | value: "grdhyy5v829zi6h8hdyoib3cfb8fm18d", 28 | expirationDate: 1798790400, 29 | hostOnly: false, 30 | httpOnly: false, 31 | secure: false, 32 | session: false, 33 | sameSite: "no_restriction", 34 | }); 35 | await page.goto(`http://localhost:8000/admin${path}`); 36 | await page.addScriptTag({ path: require.resolve("axe-core") }); 37 | // await page.screenshot({ path: "example.png" }); 38 | const result = await page.evaluate( 39 | () => 40 | new Promise((resolve) => { 41 | window.axe.run((err, results) => { 42 | if (err) throw err; 43 | resolve(results); 44 | }); 45 | }), 46 | ); 47 | const documentTitle = await page.title(); 48 | // Output the raw result object 49 | // console.log(result); 50 | 51 | issues.push({ 52 | label: label, 53 | documentTitle: documentTitle, 54 | pageUrl: result.url, 55 | result: result, 56 | }); 57 | 58 | fs.writeFileSync( 59 | `./accessibility/data/axe.json`, 60 | JSON.stringify(issues, null, 2), 61 | "utf8", 62 | ); 63 | } catch (error) { 64 | // Output an error if it occurred 65 | console.error(error.message); 66 | } 67 | } 68 | await browser.close(); 69 | }; 70 | 71 | run(); 72 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/ignoreCSP.js: -------------------------------------------------------------------------------- 1 | /** 2 | * IGNORE CSP HEADERS 3 | * Listen to all requests. If a request matches scenario.url 4 | * then fetch the request again manually, strip out CSP headers 5 | * and respond to the original request without CSP headers. 6 | * Allows `ignoreHTTPSErrors: true` BUT... requires `debugWindow: true` 7 | * 8 | * see https://github.com/GoogleChrome/puppeteer/issues/1229#issuecomment-380133332 9 | * this is the workaround until Page.setBypassCSP lands... https://github.com/GoogleChrome/puppeteer/pull/2324 10 | * 11 | * @param {REQUEST} request 12 | * @return {VOID} 13 | * 14 | * Use this in an onBefore script E.G. 15 | ``` 16 | module.exports = async function(page, scenario) { 17 | require('./removeCSP')(page, scenario); 18 | } 19 | ``` 20 | * 21 | */ 22 | 23 | const fetch = require("node-fetch"); 24 | const https = require("https"); 25 | const agent = new https.Agent({ 26 | rejectUnauthorized: false, 27 | }); 28 | 29 | module.exports = async function (page, scenario) { 30 | const intercept = async (request, targetUrl) => { 31 | const requestUrl = request.url(); 32 | 33 | // FIND TARGET URL REQUEST 34 | if (requestUrl === targetUrl) { 35 | const cookiesList = await page.cookies(requestUrl); 36 | const cookies = cookiesList 37 | .map((cookie) => `${cookie.name}=${cookie.value}`) 38 | .join("; "); 39 | const headers = Object.assign(request.headers(), { cookie: cookies }); 40 | const options = { 41 | headers: headers, 42 | body: request.postData(), 43 | method: request.method(), 44 | follow: 20, 45 | agent, 46 | }; 47 | 48 | const result = await fetch(requestUrl, options); 49 | 50 | const buffer = await result.buffer(); 51 | const cleanedHeaders = result.headers._headers || {}; 52 | cleanedHeaders["content-security-policy"] = ""; 53 | await request.respond({ 54 | body: buffer, 55 | headers: cleanedHeaders, 56 | status: result.status, 57 | }); 58 | } else { 59 | request.continue(); 60 | } 61 | }; 62 | 63 | await page.setRequestInterception(true); 64 | page.on("request", (req) => { 65 | intercept(req, scenario.url); 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /backstop/backstop.config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | const allScenarios = require("../ui/scenarios"); 4 | 5 | process.setMaxListeners(0); 6 | 7 | const WAGTAIL_SESSIONID = process.env.WAGTAIL_SESSIONID; 8 | 9 | if (!WAGTAIL_SESSIONID) { 10 | throw new ReferenceError("WAGTAIL_SESSIONID is not defined."); 11 | } 12 | 13 | const scenarios = allScenarios.reduce((list, scenario) => { 14 | const states = scenario.states || []; 15 | const newEntries = [scenario].concat(states); 16 | return list.concat(newEntries); 17 | }, []); 18 | 19 | // const FILTER = /.*rich.*/; 20 | const FILTER = null; 21 | 22 | const testScenarios = scenarios 23 | .map((s) => ({ 24 | sessionid: s.unauthenticated ? "invalid" : WAGTAIL_SESSIONID, 25 | ...s, 26 | label: `${s.category} - ${s.label}`, 27 | // emulateVisionDeficiency: "achromatopsia", 28 | // emulateMediaFeatures: [ 29 | // { name: "forced-colors", value: "active" }, 30 | // { name: "prefers-contrast", value: "more" }, 31 | // ], 32 | })) 33 | .filter((s) => !Boolean(s.skip)) 34 | .filter((s) => (FILTER ? s.label.match(FILTER) : true)); 35 | 36 | const scenarioLabels = testScenarios.map((s) => s.label); 37 | const duplicateScenarioLabels = scenarioLabels.filter( 38 | (l, i) => scenarioLabels.indexOf(l) !== i, 39 | ); 40 | 41 | if (duplicateScenarioLabels.length !== 0) { 42 | console.log(duplicateScenarioLabels); 43 | throw new Error("Two scenarios cannot use the same label"); 44 | } 45 | 46 | module.exports = { 47 | debug: false, 48 | debugWindow: false, 49 | id: "bd", 50 | viewports: [ 51 | { 52 | label: "1024x768", 53 | width: 1024, 54 | height: 768, 55 | }, 56 | ], 57 | scenarios: testScenarios, 58 | onBeforeScript: "puppeteer/onBefore.js", 59 | onReadyScript: "puppeteer/onReady.js", 60 | paths: { 61 | bitmaps_reference: "backstop/data/bitmaps_reference", 62 | bitmaps_test: "backstop/data/bitmaps_test", 63 | engine_scripts: "backstop/engine_scripts", 64 | html_report: "backstop/data/html_report", 65 | ci_report: "backstop/data/ci_report", 66 | }, 67 | report: ["browser"], 68 | engine: "puppeteer", 69 | engineOptions: { 70 | args: ["--no-sandbox"], 71 | }, 72 | asyncCaptureLimit: 5, 73 | asyncCompareLimit: 50, 74 | }; 75 | -------------------------------------------------------------------------------- /ui-benchmark/README.md: -------------------------------------------------------------------------------- 1 | # UI benchmarks with Lighthouse 2 | 3 | Set up for [Wagtail 6.3 Admin UI performance testing & benchmark #12241](https://github.com/wagtail/wagtail/issues/12241). 4 | 5 | ## How to run 6 | 7 | ```bash 8 | export TEST_ORIGIN=https://static-wagtail-v5-1.netlify.app 9 | ./node_modules/.bin/lhci collect 10 | ./node_modules/.bin/lhci upload 11 | # Reformat the data to help with comparisons between versions. 12 | perl -pi -e 's/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app\/static\//wagtail.app\/static\//g' reports/*.{json,html} 13 | perl -pi -e 's/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app\/admin\/jsi18n\//wagtail.app\/admin\/jsi18n\/\//g' reports/*.{json,html} 14 | perl -pi -e 's/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app\/admin\/sprite-[0-9a-z]+\//wagtail.app\/admin\/sprite\//g' *.{json,html} 15 | perl -pi -e 's/"url":"https:\/\/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app/"url":"https:\/\/wagtail.app/g' *.{json,html} 16 | perl -pi -e 's/"origin":"https:\/\/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app/"origin":"https:\/\/wagtail.app/g' *.{json,html} 17 | perl -pi -e 's/"origins":\["https:\/\/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app/"origins":["https:\/\/wagtail.app/g' *.{json,html} 18 | perl -pi -e 's/"name":"https:\/\/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app/"name":"https:\/\/wagtail.app/g' *.{json,html} 19 | perl -pi -e 's/,"https:\/\/static-wagtail-v[0-9]+-[0-9]+\.netlify\.app/,"https:\/\/wagtail.app/g' *.{json,html} 20 | perl -pi -e 's/wagtail\.io/wagtail.org/g' *.{json,html} 21 | ``` 22 | 23 | ## Results 24 | 25 | Results are published in [thibaudcolas/wagtail-tooling-artifacts](https://github.com/thibaudcolas/wagtail-tooling-artifacts). 26 | 27 | Here is a sample report: [Comparison the Wagtail styleguide page on Wagtail v6.2 and v6.3](https://googlechrome.github.io/lighthouse-ci/difftool/?baseReport=https://thibaudcolas.github.io/wagtail-tooling-artifacts/lighthouse-reports/static_wagtail_v6_2_netlify_app-_admin_styleguide_-2024_11_12_11_17_45.report.json&compareReport=https://thibaudcolas.github.io/wagtail-tooling-artifacts/lighthouse-reports/static_wagtail_v6_3_netlify_app-_admin_styleguide_-2024_11_12_11_27_27.report.json). 28 | 29 | See [Wagtail 6.3 performance audit metrics](https://docs.google.com/spreadsheets/d/1oAPZFdAO4wlfp_knreriRrI5WhId6JCFV5hH-Uqj6IA/edit?gid=0#gid=0) for project metrics in particular. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # ------------------------------------------------- 4 | # OS files 5 | # ------------------------------------------------- 6 | .DS_Store 7 | .DS_Store? 8 | ._* 9 | .Spotlight-V100 10 | .Trashes 11 | ehthumbs.db 12 | Thumbs.db 13 | 14 | # ------------------------------------------------- 15 | # Logs and databases 16 | # ------------------------------------------------- 17 | logs 18 | *.log 19 | npm-debug.log* 20 | *.sqlite3 21 | 22 | # ------------------------------------------------- 23 | # Runtime data and caches 24 | # ------------------------------------------------- 25 | pids 26 | *.pid 27 | *.seed 28 | *.pyc 29 | *.pyo 30 | *.pot 31 | 32 | # ------------------------------------------------- 33 | # Instrumentation and tooling 34 | # ------------------------------------------------- 35 | lib-cov 36 | coverage 37 | .coverage 38 | coverage_html_report 39 | .grunt 40 | .bundle 41 | webpack-stats.json 42 | webpack-stats.html 43 | 44 | # ------------------------------------------------- 45 | # Dependency directories 46 | # ------------------------------------------------- 47 | node_modules* 48 | python_modules* 49 | bower_components 50 | .venv 51 | venv 52 | .tox 53 | $virtualenv.tar.gz 54 | $node_modules.tar.gz 55 | 56 | # ------------------------------------------------- 57 | # Users Environment 58 | # ------------------------------------------------- 59 | .lock-wscript 60 | .idea 61 | .installed.cfg 62 | .vagrant 63 | .anaconda 64 | Vagrantfile.local 65 | .env 66 | /local 67 | local.py 68 | *.sublime-project 69 | *.sublime-workspace 70 | .vscode 71 | 72 | # ------------------------------------------------- 73 | # Generated files 74 | # ------------------------------------------------- 75 | dist 76 | build 77 | /var/static/ 78 | /var/media/ 79 | /docs/_build/ 80 | develop-eggs 81 | *.egg-info 82 | downloads 83 | media 84 | eggs 85 | parts 86 | lib64 87 | .sass-cache 88 | .mypy_cache 89 | 90 | # ------------------------------------------------- 91 | # Your own project's ignores 92 | # ------------------------------------------------- 93 | backstop/data 94 | ui/data 95 | accessibility/data 96 | backstop_data 97 | contrast-themes/data 98 | sample-reports 99 | docs-screenshots/data 100 | -------------------------------------------------------------------------------- /downloads-analysis/new-packages-this-week.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | CREATE TEMP FUNCTION toMarkdown( 3 | name STRING, 4 | version STRING, 5 | home_page STRING, 6 | project_urls ARRAY) 7 | RETURNS STRING 8 | LANGUAGE js AS """ 9 | // Convert project_urls to map for easy access 10 | const urls = {}; 11 | project_urls.forEach((entry) => { 12 | const [key, value] = entry.split(', '); 13 | urls[key.toLowerCase()] = value; 14 | }); 15 | const url = urls.changelog || urls.home || urls.homepage || urls.source || urls.repository || home_page || `https://pypi.org/project/${name}/`; 16 | return `- [${name} v${version}](${url})`; 17 | """; 18 | 19 | WITH 20 | -- Factor out the pattern logic once. 21 | allowed_packages AS ( 22 | SELECT DISTINCT name 23 | FROM `bigquery-public-data.pypi.distribution_metadata` 24 | WHERE packagetype = 'bdist_wheel' 25 | AND ( 26 | name IN ('wagtail', 'coderedcms', 'longclaw', 'django-cast', 'wagalytics', 'puput', 'ls.joyous') 27 | OR name LIKE 'wagtail%' 28 | OR name LIKE '%wagtail' 29 | ) 30 | ), 31 | latest AS ( 32 | SELECT 33 | dm.name, 34 | dm.version, 35 | dm.author, 36 | dm.home_page, 37 | dm.project_urls, 38 | dm.upload_time, 39 | toMarkdown(dm.name, dm.version, dm.home_page, dm.project_urls) AS markdown 40 | FROM `bigquery-public-data.pypi.distribution_metadata` dm 41 | JOIN allowed_packages ap 42 | ON dm.name = ap.name 43 | WHERE dm.packagetype = 'bdist_wheel' 44 | AND dm.upload_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 14 DAY) 45 | QUALIFY ROW_NUMBER() OVER (PARTITION BY dm.name ORDER BY dm.upload_time DESC) = 1 46 | ), 47 | downloads AS ( 48 | SELECT 49 | fd.project, 50 | COUNT(*) AS downloads_count 51 | FROM `bigquery-public-data.pypi.file_downloads` fd 52 | JOIN allowed_packages ap 53 | ON fd.project = ap.name 54 | WHERE fd.details.installer.name = 'pip' 55 | AND DATE(fd.timestamp) BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY) AND CURRENT_DATE() 56 | GROUP BY fd.project 57 | ) 58 | 59 | SELECT 60 | l.name, 61 | d.downloads_count, 62 | l.version AS latest_release, 63 | l.author, 64 | l.home_page, 65 | l.project_urls, 66 | l.upload_time, 67 | l.markdown 68 | FROM latest l 69 | JOIN downloads d 70 | ON l.name = d.project 71 | ORDER BY l.upload_time DESC; 72 | -------------------------------------------------------------------------------- /ui/export-components.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const scenarios = require("./scenarios"); 3 | const allComponents = require("./components"); 4 | 5 | const { convertArrayToCSV } = require("convert-array-to-csv"); 6 | 7 | const rows = []; 8 | 9 | allComponents 10 | .filter((s) => !s.skipReport) 11 | .forEach((component, i) => { 12 | const isNewCategory = 13 | i === 0 || allComponents[i - 1].category !== component.category; 14 | 15 | if (isNewCategory) { 16 | rows.push([component.category]); 17 | } 18 | 19 | const states = component.states || []; 20 | const statesLabel = states.map((state) => state.label || state).join(", "); 21 | const variations = component.variations || []; 22 | const variationsLabel = variations.map((v) => v.label || v).join(", "); 23 | const subComp = component.components || []; 24 | const subCompLabel = subComp.map((c) => c.label || c).join(", "); 25 | 26 | rows.push([ 27 | "", 28 | component.label, 29 | "", 30 | component.audience, 31 | component.frequency, 32 | subCompLabel, 33 | variationsLabel, 34 | statesLabel, 35 | component.lastUpdated, 36 | component.workNeeded, 37 | component.nextReleaseTarget, 38 | component.notes, 39 | ]); 40 | 41 | if (component.label !== "Table listing") { 42 | scenarios.forEach((scenario) => { 43 | if (scenario.components?.includes(component.label)) { 44 | rows.push(["", "", `${scenario.category} - ${scenario.label}`]); 45 | } else { 46 | const variation = scenario.components?.find((c) => 47 | c ? component.variations.includes(c?.label || c) : false, 48 | ); 49 | if (variation) { 50 | rows.push([ 51 | "", 52 | "", 53 | `${scenario.category} - ${scenario.label} (${ 54 | variation.label || variation 55 | })`, 56 | ]); 57 | } 58 | } 59 | }); 60 | } 61 | }); 62 | 63 | const csv = convertArrayToCSV(rows, { 64 | header: [ 65 | "Category", 66 | "Component", 67 | "Usage", 68 | "Audience", 69 | "Frequency", 70 | "Components", 71 | "Variations", 72 | "States", 73 | "Last updated", 74 | "Work needed", 75 | "Wagtail 4.0 target", 76 | "Notes", 77 | ], 78 | }); 79 | 80 | fs.writeFileSync(`./ui/data/components.csv`, csv, "utf8"); 81 | -------------------------------------------------------------------------------- /accessibility/pa11y-sc-report.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const { convertArrayToCSV } = require("convert-array-to-csv"); 3 | 4 | const wcag21 = require("./docs/wcag21"); 5 | const { getUniqueIssues } = require("./pa11y-dedupe"); 6 | 7 | const csvHeader = [ 8 | "Category", 9 | "Issue", 10 | "Code", 11 | "Impact", 12 | "Occurences", 13 | "Selector", 14 | "Context", 15 | "Occurences", 16 | "Screenshots", 17 | ]; 18 | 19 | const rows = []; 20 | 21 | const uniqueIssues = getUniqueIssues(); 22 | 23 | const getIssueRow = (issue) => { 24 | const { label, code, impact, context, selector, instances } = issue; 25 | 26 | const instancesLabel = issue.instances 27 | .map( 28 | (instance) => 29 | `${instance.label} (${instance.pageUrl.replace( 30 | "http://localhost:8000/admin", 31 | "", 32 | )})`, 33 | ) 34 | .join(", ") 35 | .substr(0, 100); 36 | 37 | const screenshots = issue.instances 38 | .map((instance) => instance.screenshot) 39 | .join(", ") 40 | .substr(0, 100); 41 | 42 | return [ 43 | "", 44 | label, 45 | code, 46 | impact, 47 | instances.length, 48 | selector, 49 | context, 50 | instancesLabel, 51 | screenshots, 52 | ]; 53 | }; 54 | 55 | wcag21.principles.forEach((principle) => { 56 | const principleLabel = `Principle ${principle.num} - ${principle.handle}`; 57 | rows.push([""]); 58 | rows.push([principleLabel]); 59 | rows.push([principle.title]); 60 | principle.guidelines.forEach((guideline) => { 61 | const guidelineLabel = `Guideline ${guideline.num} - ${guideline.handle}`; 62 | rows.push([""]); 63 | rows.push([guidelineLabel]); 64 | 65 | guideline.successcriteria 66 | .filter((sc) => sc.level !== "AAA") 67 | .forEach((sc) => { 68 | const scLabel = `${sc.num} ${sc.handle} - Level ${sc.level}`; 69 | rows.push([""]); 70 | rows.push([scLabel]); 71 | rows.push([""]); 72 | 73 | uniqueIssues 74 | .filter((issue) => issue?.wcagSC?.includes(sc.num)) 75 | .forEach((issue) => { 76 | rows.push(getIssueRow(issue)); 77 | }); 78 | }); 79 | }); 80 | }); 81 | 82 | rows.push([""]); 83 | rows.push(["Bonus: best practices to apply"]); 84 | 85 | uniqueIssues 86 | .filter((issue) => !issue.wcagSC) 87 | .forEach((issue) => { 88 | rows.push(getIssueRow(issue)); 89 | }); 90 | 91 | const csv = convertArrayToCSV(rows, { 92 | header: csvHeader, 93 | }); 94 | 95 | fs.writeFileSync("pa11y-sc-report.csv", csv, "utf8"); 96 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/clickAndHoverHelper.js: -------------------------------------------------------------------------------- 1 | module.exports = async (page, scenario) => { 2 | const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector; 3 | const focusSelector = scenario.focusSelectors || scenario.focusSelector; 4 | const clickSelector = scenario.clickSelectors || scenario.clickSelector; 5 | const hoverAfterClickSelector = 6 | scenario.hoverAfterClickSelectors || scenario.hoverAfterClickSelector; 7 | const keyPressSelector = 8 | scenario.keyPressSelectors || scenario.keyPressSelector; 9 | const scrollToSelector = scenario.scrollToSelector; 10 | const typeSelectSelector = scenario.typeSelectSelector; 11 | const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int] 12 | 13 | if (focusSelector) { 14 | for (const focusSelectorIndex of [].concat(focusSelector)) { 15 | // await page.waitForSelector(focusSelectorIndex); 16 | await page.focus(focusSelectorIndex); 17 | } 18 | } 19 | 20 | if (keyPressSelector) { 21 | for (const keyPressSelectorItem of [].concat(keyPressSelector)) { 22 | await page.waitForSelector(keyPressSelectorItem.selector); 23 | await page.type( 24 | keyPressSelectorItem.selector, 25 | keyPressSelectorItem.keyPress, 26 | ); 27 | } 28 | } 29 | 30 | if (typeSelectSelector) { 31 | await page.type(typeSelectSelector, "Hello, "); 32 | await page.keyboard.down("ShiftLeft"); 33 | for (let i = 0; i < "Hello, ".length; i += 1) { 34 | await page.keyboard.press("ArrowLeft"); 35 | } 36 | await new Promise((r) => setTimeout(r, 100)); 37 | } 38 | 39 | if (hoverSelector) { 40 | for (const hoverSelectorIndex of [].concat(hoverSelector)) { 41 | await page.waitForSelector(hoverSelectorIndex); 42 | await page.hover(hoverSelectorIndex); 43 | } 44 | } 45 | 46 | if (clickSelector) { 47 | for (const clickSelectorIndex of [].concat(clickSelector)) { 48 | await page.waitForSelector(clickSelectorIndex); 49 | await page.click(clickSelectorIndex); 50 | } 51 | } 52 | 53 | if (hoverAfterClickSelector) { 54 | for (const index of [].concat(hoverAfterClickSelector)) { 55 | await page.waitForSelector(index); 56 | await page.hover(index); 57 | } 58 | } 59 | 60 | if (postInteractionWait) { 61 | await new Promise((r) => setTimeout(r, postInteractionWait)); 62 | } 63 | 64 | if (scrollToSelector) { 65 | await page.waitForSelector(scrollToSelector); 66 | await page.evaluate((scrollToSelector) => { 67 | document.querySelector(scrollToSelector).scrollIntoView(); 68 | }, scrollToSelector); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /ui/export-scenarios.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const scenarios = require("./scenarios"); 3 | const allComponents = require("./components"); 4 | 5 | const { convertArrayToCSV } = require("convert-array-to-csv"); 6 | 7 | const rows = []; 8 | let lastCategory = ""; 9 | 10 | scenarios 11 | .filter((s) => !s.skipReport) 12 | .forEach((scenario, i) => { 13 | const isNewCategory = scenario.category !== lastCategory; 14 | 15 | if (isNewCategory) { 16 | rows.push([scenario.category]); 17 | lastCategory = scenario.category; 18 | } 19 | 20 | rows.push([ 21 | "", 22 | scenario.label, 23 | "", 24 | scenario.path, 25 | scenario.audience, 26 | scenario.frequency, 27 | scenario.lastUpdated, 28 | scenario.workNeeded, 29 | scenario.nextReleaseTarget, 30 | scenario.notes, 31 | `https://static-wagtail-v5-2.netlify.app/admin${scenario.path}`, 32 | ]); 33 | 34 | const components = scenario.components || []; 35 | components.forEach((label) => { 36 | const c = allComponents.find((c) => c.label === label); 37 | if (!c) { 38 | rows.push(["", "", label]); 39 | return; 40 | } 41 | 42 | rows.push([ 43 | "", 44 | "", 45 | c.label, 46 | "", 47 | c.audience && c.audience !== scenario.audience ? c.audience : "", 48 | c.frequency && c.frequency !== scenario.frequency ? c.frequency : "", 49 | c.lastUpdated, 50 | c.workNeeded, 51 | c.nextReleaseTarget, 52 | c.notes, 53 | ]); 54 | }); 55 | 56 | const states = scenario.states || []; 57 | 58 | states.forEach((s) => { 59 | // prettier-ignore 60 | rows.push([ 61 | "", 62 | s.label, 63 | "", 64 | s.path && s.path !== scenario.path ? s.path : "", 65 | s.audience && s.audience !== scenario.audience ? s.audience : "", 66 | s.frequency && s.frequency !== scenario.frequency ? s.frequency : "", 67 | s.lastUpdated && s.lastUpdated !== scenario.lastUpdated ? s.lastUpdated : "", 68 | s.workNeeded && s.workNeeded !== scenario.workNeeded ? s.workNeeded : "", 69 | s.nextReleaseTarget && s.nextReleaseTarget !== scenario.nextReleaseTarget ? s.nextReleaseTarget : "", 70 | s.notes && s.notes !== scenario.notes ? s.notes : "", 71 | ]); 72 | }); 73 | }); 74 | 75 | const csv = convertArrayToCSV(rows, { 76 | header: [ 77 | "Category", 78 | "View", 79 | "Components", 80 | "Path", 81 | "Audience", 82 | "Frequency", 83 | "Last updated", 84 | "Work needed", 85 | "Wagtail 4.0 target", 86 | "Notes", 87 | "URL", 88 | ], 89 | }); 90 | 91 | fs.writeFileSync(`./ui/data/scenarios.csv`, csv, "utf8"); 92 | -------------------------------------------------------------------------------- /accessibility/docs/success-criteria-mapping.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const axeRules = require("./axe-rules"); 3 | const htmlcsRules = require("./htmlcs-rules"); 4 | const wcag21 = require("./wcag21"); 5 | 6 | const axeSuccessCriteria = {}; 7 | Object.keys(axeRules).forEach((id) => { 8 | const rule = axeRules[id]; 9 | const wcagTags = rule.tags 10 | .filter((tag) => tag.startsWith("wcag") && !tag.endsWith("a")) 11 | .map((tag) => tag.replace("wcag", "").split("").join(".")); 12 | 13 | axeSuccessCriteria[id] = wcagTags; 14 | }); 15 | 16 | const axeStandardLevel = {}; 17 | Object.keys(axeRules).forEach((id) => { 18 | const rule = axeRules[id]; 19 | const wcagTags = rule.tags 20 | .filter((tag) => 21 | ["wcag2a", "wcag2aa", "wcag2aaa", "wcag21a", "wcag21aa"].includes(tag), 22 | ) 23 | .map((tag) => 24 | tag.replace("21a", "2.1 A").replace("2a", "2.0 A").toUpperCase(), 25 | ); 26 | 27 | axeStandardLevel[id] = wcagTags ? wcagTags[0] : null; 28 | }); 29 | 30 | const rulesMapping = {}; 31 | Object.keys(axeRules).forEach((id) => { 32 | const rule = axeRules[id]; 33 | 34 | const matches = Object.keys(htmlcsRules).filter((htmlcsId) => { 35 | const htmlcsRule = htmlcsRules[htmlcsId]; 36 | return rule.sc.includes(htmlcsRule.sc); 37 | }); 38 | 39 | // matches.forEach((match) => { 40 | // const htmlcsRule = htmlcsRules[match]; 41 | // if (htmlcsRule.standard !== rule.standard) { 42 | // console.log(htmlcsRule.standard, rule.standard); 43 | // } 44 | 45 | // if (htmlcsRule.level !== rule.level) { 46 | // console.log(htmlcsRule.level, rule.level); 47 | // } 48 | // }); 49 | 50 | rulesMapping[id] = matches; 51 | }); 52 | 53 | const scMapping = {}; 54 | 55 | wcag21.principles.forEach((principle) => { 56 | principle.guidelines.forEach((guideline) => { 57 | guideline.successcriteria.forEach((sc) => { 58 | const { id, alt_id, num, versions, level, handle, title } = sc; 59 | 60 | const rules = {}; 61 | 62 | rules.axe = Object.keys(axeRules).filter((id) => 63 | axeRules[id].sc.includes(sc.num), 64 | ); 65 | 66 | rules.htmlcs = Object.keys(htmlcsRules).filter( 67 | (id) => htmlcsRules[id].sc === sc.num, 68 | ); 69 | 70 | rules.manual = []; 71 | 72 | scMapping[sc.num] = { 73 | id, 74 | alt_id, 75 | num, 76 | versions, 77 | level, 78 | handle, 79 | title, 80 | rules, 81 | }; 82 | 83 | // if (rules.axe.length === 0 || rules.htmlcs.length === 0) { 84 | // console.log(num, title); 85 | // } 86 | }); 87 | }); 88 | }); 89 | 90 | fs.writeFileSync( 91 | "./success-criteria-mapping.json", 92 | JSON.stringify(scMapping, null, 2), 93 | "utf8", 94 | ); 95 | -------------------------------------------------------------------------------- /accessibility/pa11y-dedupe.js: -------------------------------------------------------------------------------- 1 | const automatedIssues = require("./data/pa11y.json"); 2 | 3 | const axeRules = require("./docs/axe-rules"); 4 | const htmlcsRules = require("./docs/htmlcs-rules"); 5 | const rulesMapping = require("./docs/axe-htmlcs-mapping.json"); 6 | 7 | const runnerMapping = { 8 | axe: rulesMapping, 9 | htmlcs: Object.entries(rulesMapping).reduce((mapping, entry) => { 10 | if (entry[1]) { 11 | mapping[entry[1]] = entry[0]; 12 | } 13 | return mapping; 14 | }, {}), 15 | }; 16 | // const successCriteriaMapping = require("./docs/success-criteria-mapping.json"); 17 | 18 | const runnerRules = { 19 | htmlcs: htmlcsRules, 20 | axe: axeRules, 21 | manual: {}, 22 | }; 23 | 24 | const allRules = Object.assign({}, htmlcsRules, axeRules); 25 | 26 | const issues = [ 27 | // ...manualIssues, 28 | ...automatedIssues, 29 | ]; 30 | 31 | const getUniqueIssues = () => { 32 | const uniqueIssues = issues.reduce((unique, issue) => { 33 | const { 34 | label, 35 | documentTitle, 36 | pageUrl, 37 | code, 38 | context, 39 | message, 40 | selector, 41 | runner, 42 | screenshot, 43 | } = issue; 44 | const rule = runnerRules[runner][code]; 45 | const opposingCode = runner === "manual" ? "" : runnerMapping[runner][code]; 46 | const opposingRule = allRules[opposingCode]; 47 | // const sc = Array.isArray(rule.sc) ? rule.sc : [rule.sc]; 48 | 49 | // Get a warning if a rule cannot be mapped to anything. 50 | if (!rule) { 51 | console.log(code); 52 | } 53 | 54 | let hash = `${code}${context}`; 55 | let uniqueIssue = unique[hash]; 56 | 57 | if (!uniqueIssue) { 58 | hash = `${opposingCode}${context}`; 59 | uniqueIssue = unique[hash]; 60 | } 61 | 62 | const instance = { 63 | label, 64 | documentTitle, 65 | pageUrl, 66 | screenshot, 67 | selector, 68 | }; 69 | 70 | if (uniqueIssue) { 71 | unique[hash].instances.push(instance); 72 | } else { 73 | unique[hash] = { 74 | label: message, 75 | description: "", 76 | wcagSC: rule && Array.isArray(rule.sc) ? rule.sc.join(", ") : rule?.sc, 77 | standard: rule?.standard || rule?.runner, 78 | wcagLevel: rule?.level || "Best practice", 79 | impact: rule?.impact || (opposingRule && opposingRule.impact) || "", 80 | code: code, 81 | instances: [instance], 82 | furtherDescription: "", 83 | context: context, 84 | selector: selector, 85 | analystComment: "", 86 | solutions: [], 87 | estimation: "", 88 | ticket: "", 89 | runner: runner, 90 | }; 91 | } 92 | 93 | return unique; 94 | }, {}); 95 | 96 | return Object.values(uniqueIssues); 97 | }; 98 | 99 | module.exports = { 100 | getUniqueIssues, 101 | }; 102 | -------------------------------------------------------------------------------- /downloads-analysis/README.md: -------------------------------------------------------------------------------- 1 | # Downloads analysis 2 | 3 | Tools related to analyzing Wagtail downloads. Dataset: [PyPI on BigQuery](https://cloud.google.com/blog/topics/developers-practitioners/analyzing-python-package-downloads-bigquery) 4 | 5 | ## Release version numbers and dates 6 | 7 | ```bash 8 | curl https://packages.ecosyste.ms/api/v1/registries/pypi.org/packages/wagtail/versions\?per_page\=500 > wagtail-versions.json 9 | # Only keep numbers and dates. 10 | cat wagtail-versions.json | jq '.[] | "\(.number): \(.published_at)"' > wagtail-versions.txt 11 | ``` 12 | 13 | ## Release candidate downloads 14 | 15 | Produce [Wagtail release statistics](https://docs.google.com/spreadsheets/d/1eZ3OvpoHza1lSRzznZLh2qdbDE-RuhTmImndqg0Ugwk/edit) to understand usage patterns over time. 16 | 17 | ```bash 18 | # Update to use the desired version number, then dry run. 19 | cat release-candidate-downloads.sql | bq query --use_legacy_sql=false --dry_run 2>&1 | grep -o '[0-9]\+' | awk '{printf "%.2f GB\n", $1/1024/1024/1024}' 20 | cat release-candidate-downloads.sql | bq query --use_legacy_sql=false --format=csv > release-candidate-downloads.csv 21 | # Update to use the desired version number, then dry run. 22 | cat release-candidate-total.sql | bq query --use_legacy_sql=false --dry_run 2>&1 | grep -o '[0-9]\+' | awk '{printf "%.2f GB\n", $1/1024/1024/1024}' 23 | cat release-candidate-total.sql | bq query --use_legacy_sql=false --format=csv > release-candidate-total.csv 24 | ``` 25 | 26 | ## New packages this week 27 | 28 | Warning: expensive! 29 | 30 | ```bash 31 | cat new-packages-this-week.sql | bq query --max_rows 500 --use_legacy_sql=false --dry_run 2>&1 | grep -o '[0-9]\+' | awk '{printf "%.2f GB\n", $1/1024/1024/1024}' 32 | ``` 33 | 34 | ## Installer stats 35 | 36 | See [pip poetry uv for Wagtail - installer statistics](https://docs.google.com/spreadsheets/d/14fval60fdh9YJftg3ysPpCcpX44kvTsr0MBbuAFPKQ4/edit?usp=sharing) 37 | 38 | ## CI installer stats 39 | 40 | ⚠️ Experimental - data interpretation is unclear, see [uv overtakes pip in CI (for Wagtail users)](https://wagtail.org/blog/uv-overtakes-pip-in-ci/) for more information. 41 | 42 | See [pip poetry uv for Wagtail - installer statistics](https://docs.google.com/spreadsheets/d/14fval60fdh9YJftg3ysPpCcpX44kvTsr0MBbuAFPKQ4/edit?usp=sharing), and on the Python forum: [PyPI downloads statistics and continuous integration](https://discuss.python.org/t/pypi-downloads-statistics-and-continuous-integration/91810). 43 | 44 | ### What "CI" means 45 | 46 | The "CI" True/null metadata is only available with pip and uv. There is no opportunity to conclusively determine an install isn’t in CI, so any "CI = true" numbers are a likely lower bound. 47 | 48 | - It’s based on detect the following environment variables: `BUILD_BUILDID, BUILD_ID, CI, PIP_IS_CI` 49 | - Implementation in uv: [looks_like_ci in linehaul.rs](https://github.com/astral-sh/uv/blob/0.7.3/crates/uv-client/src/linehaul.rs#L66-L69) 50 | - Implementation in pip: [looks_like_ci in session.py](https://github.com/pypa/pip/blob/25.1.1/src/pip/_internal/network/session.py#L81-L107) 51 | - Early discussion about the need for this in pip / PyPI: [Differentiating organic vs automated installations pypa/pip#5499](https://github.com/pypa/pip/issues/5499) 52 | -------------------------------------------------------------------------------- /backstop/engine_scripts/puppeteer/onReady.js: -------------------------------------------------------------------------------- 1 | module.exports = async (page, scenario, viewport) => { 2 | console.log("SCENARIO > " + scenario.label); 3 | await require("./loadSVG")(page, scenario); 4 | 5 | await page.evaluate(() => { 6 | const consoleWarn = console.warn; 7 | 8 | console.warn = function filterWarnings(msg, ...args) { 9 | // Stop logging React warnings we shouldn’t be doing anything about at this time. 10 | const supressedWarnings = [ 11 | "Warning: componentWillMount", 12 | "Warning: componentWillReceiveProps", 13 | "Warning: componentWillUpdate", 14 | ]; 15 | 16 | if (!supressedWarnings.some((entry) => msg.includes(entry))) { 17 | consoleWarn.apply(console, ...args); 18 | } 19 | }; 20 | 21 | window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true }; 22 | 23 | const preventInteractionStyles = ` 24 | * { 25 | cursor: none !important; 26 | user-select: none !important; 27 | animation-delay: 0.01s !important; 28 | animation-duration: 0.01s !important; 29 | transition-property: none !important; 30 | }`; 31 | 32 | const style = document.createElement("style"); 33 | style.innerHTML = preventInteractionStyles; 34 | document.body.appendChild(style); 35 | 36 | // TODO Break the tests if the user is not logged in – no point in testing. 37 | 38 | // Makes dates ("time since") static. 39 | [].slice 40 | .call(document.querySelectorAll(".w-human-readable-date")) 41 | .forEach((date) => { 42 | date.innerHTML = "24 minutes ago"; 43 | }); 44 | 45 | // Override version number in the dashboard. 46 | Array.from( 47 | document.querySelectorAll('[href="https://guide.wagtail.org/"]'), 48 | ).forEach((link) => { 49 | if (link.innerText.includes("editor guide")) { 50 | link.innerHTML = link.innerHTML.replace( 51 | /Wagtail [^\s]+ editor guide/, 52 | "Wagtail editor guide", 53 | ); 54 | } 55 | }); 56 | }); 57 | 58 | // if (scenario.clickSelector) { 59 | // const clickSelector = require("./clickSelector"); 60 | // await clickSelector(page, scenario, viewport); 61 | // } 62 | await require("./clickAndHoverHelper")(page, scenario); 63 | 64 | if (scenario.highlightSelector) { 65 | await page.evaluate((selector) => { 66 | const style = document.createElement("style"); 67 | style.innerHTML = ` 68 | ${selector}, :is(${selector}):is(:hover, :active, :focus, :focus-visible) { 69 | outline: 5px solid red !important; 70 | outline-offset: 2px !important; 71 | } 72 | `; 73 | document.body.appendChild(style); 74 | }, scenario.highlightSelector); 75 | } 76 | 77 | if (scenario.highlightInsideSelector) { 78 | await page.evaluate((selector) => { 79 | const style = document.createElement("style"); 80 | style.innerHTML = ` 81 | ${selector}, ${selector}:is(:hover, :active, :focus, :focus-visible) { 82 | outline: 5px solid red !important; 83 | outline-offset: -5px !important; 84 | } 85 | `; 86 | document.body.appendChild(style); 87 | }, scenario.highlightInsideSelector); 88 | } 89 | 90 | // add more ready handlers here... 91 | }; 92 | -------------------------------------------------------------------------------- /accessibility/docs/linting.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "Parse error: expected 'p' at 7:2", 4 | "context": "wagtail/admin/templates/wagtailadmin/collection_privacy/ancestor_privacy.html:8:2" 5 | }, 6 | { 7 | "label": "Parse error: expected '>' at 25:150", 8 | "context": "wagtail/admin/templates/wagtailadmin/edit_handlers/chooser_panel.html:26:150" 9 | }, 10 | { 11 | "label": "Parse error: expected 'p' at 7:2", 12 | "context": "wagtail/admin/templates/wagtailadmin/page_privacy/ancestor_privacy.html:8:2" 13 | }, 14 | { 15 | "label": "Parse error: expected 'ul' at 14:6", 16 | "context": "wagtail/admin/templates/wagtailadmin/pages/listing/_button_with_dropdown.html:15:6" 17 | }, 18 | { 19 | "label": "Parse error: expected 'p' at 68:271", 20 | "context": "wagtail/admin/templates/wagtailadmin/pages/listing/_list_explore.html:69:271" 21 | }, 22 | { 23 | "label": "Parse error: expected 'tbody' at 42:2", 24 | "context": "wagtail/admin/templates/wagtailadmin/permissions/includes/collection_member_permissions_formset.html:43:2" 25 | }, 26 | { 27 | "label": "Parse error: expected 'td' at 33:14", 28 | "context": "wagtail/contrib/forms/templates/wagtailforms/list_submissions.html:34:14" 29 | }, 30 | { 31 | "label": "Parse error: expected one of 'autoescape', 'block', 'blocktrans', 'comment', 'filter', 'for', 'if', 'ifchanged', 'ifequal', 'ifnotequal', 'not an intermediate Jinja tag name', 'spaceless', 'verbatim', 'with' at 18:8", 32 | "context": "wagtail/contrib/redirects/templates/wagtailredirects/results.html:19:8" 33 | }, 34 | { 35 | "label": "Parse error: expected one of 'autoescape', 'block', 'blocktrans', 'comment', 'filter', 'for', 'if', 'ifchanged', 'ifequal', 'ifnotequal', 'not an intermediate Jinja tag name', 'spaceless', 'verbatim', 'with' at 433:205", 36 | "context": "wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html:434:205" 37 | }, 38 | { 39 | "label": "Parse error: expected one of '[:a-z]', 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'script', 'source', 'style', 'track', 'wbr', '{#', '{%', '{{' at 65:5", 40 | "context": "wagtail/documents/templates/wagtaildocs/documents/edit.html:66:5" 41 | }, 42 | { 43 | "label": "Parse error: expected 'p' at 39:10", 44 | "context": "wagtail/documents/templates/wagtaildocs/multiple/add.html:40:10" 45 | }, 46 | { 47 | "label": "Parse error: expected 'p' at 40:10", 48 | "context": "wagtail/images/templates/wagtailimages/multiple/add.html:41:10" 49 | }, 50 | { 51 | "label": "Parse error: expected one of '=', '>', '[0-9-_.]', '[:a-z]', '\\s+', '{#', '{%', '{{' at 5:70", 52 | "context": "wagtail/project_template/home/templates/home/welcome_page.html:6:70" 53 | }, 54 | { 55 | "label": "Parse error: expected one of 'autoescape', 'block', 'blocktrans', 'comment', 'endif', 'filter', 'for', 'if', 'ifchanged', 'ifequal', 'ifnotequal', 'not an intermediate Jinja tag name', 'spaceless', 'verbatim', 'with' at 12:11", 56 | "context": "wagtail/snippets/templates/wagtailsnippets/snippets/edit.html:13:11" 57 | }, 58 | { 59 | "label": "Parse error: expected 'tbody' at 42:2", 60 | "context": "wagtail/users/templates/wagtailusers/groups/includes/page_permissions_formset.html:43:2" 61 | }, 62 | { 63 | "label": "Parse error: expected 'tr' at 17:14", 64 | "context": "wagtail/users/templates/wagtailusers/groups/list.html:18:14" 65 | } 66 | ] 67 | -------------------------------------------------------------------------------- /downloads-analysis/release-candidate-total.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | WITH downloads AS ( 3 | SELECT * 4 | FROM `bigquery-public-data.pypi.file_downloads` as dl 5 | WHERE dl.project = 'wagtail' 6 | -- 7.2 7 | -- AND dl.timestamp BETWEEN TIMESTAMP('2025-10-23T22:31:32.000Z') AND TIMESTAMP('2025-11-05T12:27:46.000Z') 8 | -- 7.1 9 | -- AND dl.timestamp BETWEEN TIMESTAMP('2025-07-24T14:50:46.000Z') AND TIMESTAMP('2025-08-04T15:25:15.000Z') 10 | -- 7.0 11 | -- AND dl.timestamp BETWEEN TIMESTAMP('2025-04-24T21:07:38.000Z') AND TIMESTAMP('2025-05-06T15:41:29.000Z') 12 | -- 6.4 13 | -- AND dl.timestamp BETWEEN TIMESTAMP('2025-01-20T18:49:20.000Z') AND TIMESTAMP('2025-02-03T17:09:01.000Z') 14 | -- 6.3 15 | -- AND dl.timestamp BETWEEN TIMESTAMP('2024-10-21T17:18:36.000Z') AND TIMESTAMP('2024-11-01T14:01:24.000Z') 16 | -- 6.2 17 | -- AND dl.timestamp BETWEEN TIMESTAMP('2024-07-19T16:23:26.000Z') AND TIMESTAMP('2024-08-01T13:29:29.000Z') 18 | -- 6.1 19 | -- AND dl.timestamp BETWEEN TIMESTAMP('2024-04-18T16:59:25.000Z') AND TIMESTAMP('2024-05-01T13:10:29.000Z') 20 | -- 6.0 21 | -- AND dl.timestamp BETWEEN TIMESTAMP('2024-01-24T14:56:22.000Z') AND TIMESTAMP('2024-02-07T13:22:20.000Z') 22 | -- 5.2 23 | -- AND dl.timestamp BETWEEN TIMESTAMP('2023-10-19T22:41:09.000Z') AND TIMESTAMP('2023-11-01T12:55:57.000Z') 24 | -- 5.1 25 | -- AND dl.timestamp BETWEEN TIMESTAMP('2023-07-18T15:19:19.000Z') AND TIMESTAMP('2023-08-01T13:54:27.000Z') 26 | -- 5.0 27 | -- AND dl.timestamp BETWEEN TIMESTAMP('2023-04-20T11:24:46.000Z') AND TIMESTAMP('2023-05-02T15:07:54.000Z') 28 | -- 4.2 29 | -- AND dl.timestamp BETWEEN TIMESTAMP('2023-01-20T13:55:50.000Z') AND TIMESTAMP('2023-02-06T13:48:27.000Z') 30 | -- 4.1 31 | -- AND dl.timestamp BETWEEN TIMESTAMP('2022-10-18T12:00:42.000Z') AND TIMESTAMP('2022-11-01T11:51:51.000Z') 32 | -- 4.0 33 | -- AND dl.timestamp BETWEEN TIMESTAMP('2022-08-12T13:32:37.000Z') AND TIMESTAMP('2022-08-31T13:38:20.000Z') 34 | -- 3.0 35 | -- AND dl.timestamp BETWEEN TIMESTAMP('2022-04-14T12:49:27.000Z') AND TIMESTAMP('2022-05-16T14:02:05.000Z') 36 | -- 2.16 37 | -- AND dl.timestamp BETWEEN TIMESTAMP('2022-01-21T14:40:53.000Z') AND TIMESTAMP('2022-02-07T14:42:21.000Z') 38 | -- 2.15 39 | -- AND dl.timestamp BETWEEN TIMESTAMP('2021-10-15T17:04:00.000Z') AND TIMESTAMP('2021-11-04T11:51:53.000Z') 40 | -- 2.14 41 | -- AND dl.timestamp BETWEEN TIMESTAMP('2021-07-13T13:13:54.000Z') AND TIMESTAMP('2021-08-02T13:21:47.000Z') 42 | -- 2.13 43 | -- AND dl.timestamp BETWEEN TIMESTAMP('2021-04-20T20:01:07.000Z') AND TIMESTAMP('2021-05-12T14:18:05.000Z') 44 | -- 2.12 45 | -- AND dl.timestamp BETWEEN TIMESTAMP('2021-01-18T18:00:30.000Z') AND TIMESTAMP('2021-02-02T16:47:42.000') 46 | ) 47 | SELECT 48 | -- "7.2" as pre_release, 49 | -- "7.1" as pre_release, 50 | -- "7.0" as pre_release, 51 | -- "6.4" as pre_release, 52 | -- "6.3" as pre_release, 53 | -- "6.2" as pre_release, 54 | -- "6.1" as pre_release, 55 | -- "6.0" as pre_release, 56 | -- "5.2" as pre_release, 57 | -- "5.1" as pre_release, 58 | -- "5.0" as pre_release, 59 | -- "4.2" as pre_release, 60 | -- "4.1" as pre_release, 61 | -- "4.0" as pre_release, 62 | -- "3.0" as pre_release, 63 | -- "2.16" as pre_release, 64 | -- "2.15" as pre_release, 65 | -- "2.14" as pre_release, 66 | -- "2.13" as pre_release, 67 | -- "2.12" as pre_release, 68 | dl.file.version AS version, 69 | COUNT(*) AS downloads_count 70 | FROM downloads AS dl 71 | GROUP BY version 72 | ORDER BY version DESC; 73 | -------------------------------------------------------------------------------- /csp/src/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createPlaywrightRouter, 3 | PlaywrightCrawler, 4 | Dataset, 5 | EnqueueStrategy, 6 | } from "crawlee"; 7 | 8 | const TEST_ORIGIN = process.env.TEST_ORIGIN || "http://localhost:8000"; 9 | const startUrls = [ 10 | "/", 11 | "/admin/", 12 | "/admin/pages/1/add_subpage/", 13 | "/admin/pages/usage/base/standardpage/", 14 | "/admin/pages/82/move/", 15 | "/admin/pages/82/copy/", 16 | "/admin/pages/82/unpublish/", 17 | "/admin/pages/82/history/", 18 | "/admin/pages/82/revisions/compare/60...110/", 19 | "/admin/pages/60/edit/", 20 | "/admin/pages/60/", 21 | "/admin/pages/search/", 22 | "/admin/snippets/", 23 | "/admin/snippets/base/person/", 24 | "/admin/snippets/base/person/edit/1/", 25 | "/admin/snippets/base/person/history/1/", 26 | "/admin/images/", 27 | "/admin/images/47/", 28 | "/admin/images/multiple/add/", 29 | "/admin/images/43/generate_url/", 30 | "/admin/bulk/wagtailimages/image/add_tags/?p=3&id=52&id=53", 31 | "/admin/bulk/wagtailimages/image/add_to_collection/?p=3&id=53", 32 | "/admin/documents/", 33 | "/admin/documents/edit/1/", 34 | "/admin/documents/multiple/add/", 35 | "/admin/forms/", 36 | "/admin/forms/submissions/69/", 37 | "/admin/reports/locked/", 38 | "/admin/reports/workflow/", 39 | "/admin/reports/workflow_tasks/", 40 | "/admin/reports/site-history/", 41 | "/admin/reports/aging-pages/", 42 | "/admin/workflows/list/", 43 | "/admin/users/", 44 | "/admin/groups/", 45 | "/admin/sites/", 46 | "/admin/locales/", 47 | "/admin/collections/", 48 | "/admin/redirects/", 49 | "/admin/searchpicks/", 50 | "/admin/searchpicks/4/", 51 | "/admin/searchpicks/add/", 52 | "/admin/404/", 53 | ].map((path) => `${TEST_ORIGIN}${path}`); 54 | 55 | console.log(TEST_ORIGIN); 56 | 57 | const router = createPlaywrightRouter(); 58 | const dataset = await Dataset.open("csp-scanner"); 59 | 60 | router.addDefaultHandler(async ({ enqueueLinks, log, page }) => { 61 | await enqueueLinks({ 62 | // strategy: EnqueueStrategy.SameDomain, 63 | globs: [`${TEST_ORIGIN}/**`], 64 | label: "detail", 65 | }); 66 | }); 67 | 68 | router.addHandler("detail", async ({ request, page, log, pushData }) => { 69 | const title = await page.title(); 70 | log.info(`${title}`, { url: request.loadedUrl }); 71 | }); 72 | 73 | const crawler = new PlaywrightCrawler({ 74 | requestHandler: router, 75 | // Comment this option to scrape the full website. 76 | maxRequestsPerCrawl: 2000, 77 | preNavigationHooks: [ 78 | async ({ page, request }) => { 79 | await page.setExtraHTTPHeaders({ 80 | "X-Auto-Login": "admin", 81 | }); 82 | 83 | page.on("console", async (msg) => { 84 | if (msg.text().includes("Content Security Policy")) { 85 | const loc = msg.location(); 86 | const message = msg.text(); 87 | await dataset.pushData({ 88 | url: request.url.replace(TEST_ORIGIN, ""), 89 | file: loc.url 90 | .replace(TEST_ORIGIN, "") 91 | .replace("/static", "") 92 | .replace(/\?v=\w+/, ""), 93 | line: loc.lineNumber, 94 | col: loc.columnNumber, 95 | message: message.includes("style-src") 96 | ? "style-src" 97 | : message.includes("script-src") 98 | ? "script-src" 99 | : message 100 | .replace( 101 | /because it violates the following Content Security Policy directive.+/, 102 | "", 103 | ) 104 | .trim(), 105 | }); 106 | } 107 | }); 108 | }, 109 | ], 110 | }); 111 | 112 | await crawler.run(startUrls); 113 | 114 | await dataset.exportToCSV("csp-scan-results"); 115 | -------------------------------------------------------------------------------- /http-archive/legacy/Tranco site ranking.sql: -------------------------------------------------------------------------------- 1 | SELECT date, domain, rank FROM `tranco.daily.daily` WHERE domain in ( 2 | 'angular.dev', 3 | 'astro.build', 4 | 'djangoproject.com', 5 | 'drupal.org', 6 | 'expressjs.com', 7 | 'fastapi.tiangolo.com', 8 | 'fastify.io', 9 | 'flask.palletsprojects.com', 10 | 'htmx.org', 11 | 'laravel.com', 12 | 'nextjs.org', 13 | 'nuxt.com', 14 | 'phoenixframework.org', 15 | 'react.dev', 16 | 'strapi.io', 17 | 'svelte.dev', 18 | 'symfony.com', 19 | 'rubyonrails.org', 20 | 'vuejs.org', 21 | 'wordpress.org', 22 | 'wagtail.org') and 23 | ( 24 | date="2019-07-16" or 25 | date="2019-07-30" or 26 | date="2019-08-13" or 27 | date="2019-08-27" or 28 | date="2019-09-10" or 29 | date="2019-09-24" or 30 | date="2019-10-08" or 31 | date="2019-10-22" or 32 | date="2019-11-05" or 33 | date="2019-11-19" or 34 | date="2019-12-03" or 35 | date="2019-12-17" or 36 | date="2019-12-31" or 37 | date="2020-01-14" or 38 | date="2020-01-28" or 39 | date="2020-02-11" or 40 | date="2020-02-25" or 41 | date="2020-03-10" or 42 | date="2020-03-24" or 43 | date="2020-04-07" or 44 | date="2020-04-21" or 45 | date="2020-05-05" or 46 | date="2020-05-19" or 47 | date="2020-06-02" or 48 | date="2020-06-16" or 49 | date="2020-06-30" or 50 | date="2020-07-14" or 51 | date="2020-07-28" or 52 | date="2020-08-11" or 53 | date="2020-08-25" or 54 | date="2020-09-08" or 55 | date="2020-09-22" or 56 | date="2020-10-06" or 57 | date="2020-10-20" or 58 | date="2020-11-03" or 59 | date="2020-11-17" or 60 | date="2020-12-01" or 61 | date="2020-12-15" or 62 | date="2020-12-29" or 63 | date="2021-01-12" or 64 | date="2021-01-26" or 65 | date="2021-02-09" or 66 | date="2021-02-23" or 67 | date="2021-03-09" or 68 | date="2021-03-23" or 69 | date="2021-04-06" or 70 | date="2021-04-20" or 71 | date="2021-05-04" or 72 | date="2021-05-18" or 73 | date="2021-06-01" or 74 | date="2021-06-15" or 75 | date="2021-06-29" or 76 | date="2021-07-13" or 77 | date="2021-07-27" or 78 | date="2021-08-10" or 79 | date="2021-08-24" or 80 | date="2021-09-07" or 81 | date="2021-09-21" or 82 | date="2021-10-05" or 83 | date="2021-10-19" or 84 | date="2021-11-02" or 85 | date="2021-11-16" or 86 | date="2021-11-30" or 87 | date="2021-12-14" or 88 | date="2021-12-28" or 89 | date="2022-01-11" or 90 | date="2022-01-25" or 91 | date="2022-02-08" or 92 | date="2022-02-22" or 93 | date="2022-03-08" or 94 | date="2022-03-22" or 95 | date="2022-04-05" or 96 | date="2022-04-19" or 97 | date="2022-05-03" or 98 | date="2022-05-17" or 99 | date="2022-05-31" or 100 | date="2022-06-14" or 101 | date="2022-06-28" or 102 | date="2022-07-12" or 103 | date="2022-07-26" or 104 | date="2022-08-09" or 105 | date="2022-08-23" or 106 | date="2022-09-06" or 107 | date="2022-09-20" or 108 | date="2022-10-04" or 109 | date="2022-10-18" or 110 | date="2022-11-01" or 111 | date="2022-11-15" or 112 | date="2022-11-29" or 113 | date="2022-12-13" or 114 | date="2022-12-27" or 115 | date="2023-01-10" or 116 | date="2023-01-24" or 117 | date="2023-02-07" or 118 | date="2023-02-21" or 119 | date="2023-03-07" or 120 | date="2023-03-21" or 121 | date="2023-04-04" or 122 | date="2023-04-18" or 123 | date="2023-05-02" or 124 | date="2023-05-16" or 125 | date="2023-05-30" or 126 | date="2023-06-13" or 127 | date="2023-06-27" or 128 | date="2023-07-11" or 129 | date="2023-07-25" or 130 | date="2023-08-08" or 131 | date="2023-08-22" or 132 | date="2023-09-05" or 133 | date="2023-09-19" or 134 | date="2023-10-03" or 135 | date="2023-10-17" or 136 | date="2023-10-31" or 137 | date="2023-11-14" or 138 | date="2023-11-28" or 139 | date="2023-12-12" or 140 | date="2023-12-26" or 141 | date="2024-01-09" or 142 | date="2024-01-23" or 143 | date="2024-02-06" or 144 | date="2024-02-20" or 145 | date="2024-03-05" or 146 | date="2024-03-19" or 147 | date="2024-04-02" or 148 | date="2024-04-16" or 149 | date="2024-04-30" or 150 | date="2024-05-14" or 151 | date="2024-05-28" or 152 | date="2024-06-11" or 153 | date="2024-06-25" or 154 | date="2024-07-09" or 155 | date="2024-07-23" or 156 | date="2024-08-08" or 157 | date="2024-08-22" or 158 | date="2024-09-05" or 159 | date="2024-09-19" 160 | 161 | ) 162 | order by date desc, domain 163 | LIMIT 100000 164 | -------------------------------------------------------------------------------- /downloads-analysis/release-candidate-downloads.sql: -------------------------------------------------------------------------------- 1 | #standardSQL 2 | -- https://docs.google.com/spreadsheets/d/1eZ3OvpoHza1lSRzznZLh2qdbDE-RuhTmImndqg0Ugwk/edit 3 | WITH downloads AS ( 4 | SELECT * 5 | FROM `bigquery-public-data.pypi.file_downloads` as dl 6 | WHERE dl.project = 'wagtail' 7 | -- Bytes processed target per query: 0.5 - 1GB (latest: 627 MB). Dry-run estimate: 500 GB 8 | -- 7.2 9 | AND REGEXP_CONTAINS(dl.file.version, r'^(7\.2|7\.1($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2025-10-23T22:31:32.000Z') AND TIMESTAMP('2025-11-05T12:27:46.000Z') 10 | -- 7.1 11 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(7\.1|7\.0($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2025-07-24T14:50:46.000Z') AND TIMESTAMP('2025-08-04T15:25:15.000Z') 12 | -- 7.0 13 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(7\.0|6\.4($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2025-04-24T21:07:38.000Z') AND TIMESTAMP('2025-05-06T15:41:29.000Z') 14 | -- 6.4 15 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(6\.4|6\.3($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2025-01-20T18:49:20.000Z') AND TIMESTAMP('2025-02-03T17:09:01.000Z') 16 | -- 6.3 17 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(6\.3|6\.2($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2024-10-21T17:18:36.000Z') AND TIMESTAMP('2024-11-01T14:01:24.000Z') 18 | -- 6.2 19 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(6\.2|6\.1($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2024-07-19T16:23:26.000Z') AND TIMESTAMP('2024-08-01T13:29:29.000Z') 20 | -- 6.1 21 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(6\.1|6\.0($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2024-04-18T16:59:25.000Z') AND TIMESTAMP('2024-05-01T13:10:29.000Z') 22 | -- 6.0 23 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(6\.0|5\.2($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2024-01-24T14:56:22.000Z') AND TIMESTAMP('2024-02-07T13:22:20.000Z') 24 | -- 5.2 25 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(5\.2|5\.1($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2023-10-19T22:41:09.000Z') AND TIMESTAMP('2023-11-01T12:55:57.000Z') 26 | -- 5.1 27 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(5\.1|5\.0($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2023-07-18T15:19:19.000Z') AND TIMESTAMP('2023-08-01T13:54:27.000Z') 28 | -- 5.0 29 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(5\.0|4\.2($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2023-04-20T11:24:46.000Z') AND TIMESTAMP('2023-05-02T15:07:54.000Z') 30 | -- 4.2 31 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(4\.2|4\.1($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2023-01-20T13:55:50.000Z') AND TIMESTAMP('2023-02-06T13:48:27.000Z') 32 | -- 4.1 33 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(4\.1|4\.0($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2022-10-18T12:00:42.000Z') AND TIMESTAMP('2022-11-01T11:51:51.000Z') 34 | -- 4.0 35 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(4\.0|3\.0($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2022-08-12T13:32:37.000Z') AND TIMESTAMP('2022-08-31T13:38:20.000Z') 36 | -- 3.0 37 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(3\.0|2\.16($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2022-04-14T12:49:27.000Z') AND TIMESTAMP('2022-05-16T14:02:05.000Z') 38 | -- 2.16 39 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(2\.16|2\.15($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2022-01-21T14:40:53.000Z') AND TIMESTAMP('2022-02-07T14:42:21.000Z') 40 | -- 2.15 41 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(2\.15|2\.14($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2021-10-15T17:04:00.000Z') AND TIMESTAMP('2021-11-04T11:51:53.000Z') 42 | -- 2.14 43 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(2\.14|2\.13($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2021-07-13T13:13:54.000Z') AND TIMESTAMP('2021-08-02T13:21:47.000Z') 44 | -- 2.13 45 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(2\.13|2\.12($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2021-04-20T20:01:07.000Z') AND TIMESTAMP('2021-05-12T14:18:05.000Z') 46 | -- 2.12 47 | -- AND REGEXP_CONTAINS(dl.file.version, r'^(2\.12|2\.11($|\.))') AND dl.timestamp BETWEEN TIMESTAMP('2021-01-18T18:00:30.000Z') AND TIMESTAMP('2021-02-02T16:47:42.000') 48 | ) 49 | SELECT 50 | dl.file.version AS version, 51 | COUNT(*) AS downloads_count 52 | FROM downloads AS dl 53 | GROUP BY version 54 | ORDER BY version DESC; 55 | -------------------------------------------------------------------------------- /downloads-analysis/versions-share/pivot_downloads.sql: -------------------------------------------------------------------------------- 1 | create table pivot_downloads as 2 | SELECT 3 | day, 4 | SUM(total_downloads) AS total_downloads, 5 | SUM(CASE WHEN minor_version = '0.1' THEN total_downloads ELSE 0 END) AS "0.1", 6 | SUM(CASE WHEN minor_version = '0.2' THEN total_downloads ELSE 0 END) AS "0.2", 7 | SUM(CASE WHEN minor_version = '0.3' THEN total_downloads ELSE 0 END) AS "0.3", 8 | SUM(CASE WHEN minor_version = '0.4' THEN total_downloads ELSE 0 END) AS "0.4", 9 | SUM(CASE WHEN minor_version = '0.5' THEN total_downloads ELSE 0 END) AS "0.5", 10 | SUM(CASE WHEN minor_version = '0.6' THEN total_downloads ELSE 0 END) AS "0.6", 11 | SUM(CASE WHEN minor_version = '0.7' THEN total_downloads ELSE 0 END) AS "0.7", 12 | SUM(CASE WHEN minor_version = '0.8' THEN total_downloads ELSE 0 END) AS "0.8", 13 | SUM(CASE WHEN minor_version = '1.0' THEN total_downloads ELSE 0 END) AS "1.0", 14 | SUM(CASE WHEN minor_version = '1.1' THEN total_downloads ELSE 0 END) AS "1.1", 15 | SUM(CASE WHEN minor_version = '1.2' THEN total_downloads ELSE 0 END) AS "1.2", 16 | SUM(CASE WHEN minor_version = '1.3' THEN total_downloads ELSE 0 END) AS "1.3", 17 | SUM(CASE WHEN minor_version = '1.4' THEN total_downloads ELSE 0 END) AS "1.4", 18 | SUM(CASE WHEN minor_version = '1.5' THEN total_downloads ELSE 0 END) AS "1.5", 19 | SUM(CASE WHEN minor_version = '1.6' THEN total_downloads ELSE 0 END) AS "1.6", 20 | SUM(CASE WHEN minor_version = '1.7' THEN total_downloads ELSE 0 END) AS "1.7", 21 | SUM(CASE WHEN minor_version = '1.8' THEN total_downloads ELSE 0 END) AS "1.8", 22 | SUM(CASE WHEN minor_version = '1.9' THEN total_downloads ELSE 0 END) AS "1.9", 23 | SUM(CASE WHEN minor_version = '1.10' THEN total_downloads ELSE 0 END) AS "1.10", 24 | SUM(CASE WHEN minor_version = '1.11' THEN total_downloads ELSE 0 END) AS "1.11", 25 | SUM(CASE WHEN minor_version = '1.12' THEN total_downloads ELSE 0 END) AS "1.12", 26 | SUM(CASE WHEN minor_version = '1.13' THEN total_downloads ELSE 0 END) AS "1.13", 27 | SUM(CASE WHEN minor_version = '2.0' THEN total_downloads ELSE 0 END) AS "2.0", 28 | SUM(CASE WHEN minor_version = '2.1' THEN total_downloads ELSE 0 END) AS "2.1", 29 | SUM(CASE WHEN minor_version = '2.2' THEN total_downloads ELSE 0 END) AS "2.2", 30 | SUM(CASE WHEN minor_version = '2.3' THEN total_downloads ELSE 0 END) AS "2.3", 31 | SUM(CASE WHEN minor_version = '2.4' THEN total_downloads ELSE 0 END) AS "2.4", 32 | SUM(CASE WHEN minor_version = '2.5' THEN total_downloads ELSE 0 END) AS "2.5", 33 | SUM(CASE WHEN minor_version = '2.6' THEN total_downloads ELSE 0 END) AS "2.6", 34 | SUM(CASE WHEN minor_version = '2.7' THEN total_downloads ELSE 0 END) AS "2.7", 35 | SUM(CASE WHEN minor_version = '2.8' THEN total_downloads ELSE 0 END) AS "2.8", 36 | SUM(CASE WHEN minor_version = '2.9' THEN total_downloads ELSE 0 END) AS "2.9", 37 | SUM(CASE WHEN minor_version = '2.10' THEN total_downloads ELSE 0 END) AS "2.10", 38 | SUM(CASE WHEN minor_version = '2.11' THEN total_downloads ELSE 0 END) AS "2.11", 39 | SUM(CASE WHEN minor_version = '2.12' THEN total_downloads ELSE 0 END) AS "2.12", 40 | SUM(CASE WHEN minor_version = '2.13' THEN total_downloads ELSE 0 END) AS "2.13", 41 | SUM(CASE WHEN minor_version = '2.14' THEN total_downloads ELSE 0 END) AS "2.14", 42 | SUM(CASE WHEN minor_version = '2.15' THEN total_downloads ELSE 0 END) AS "2.15", 43 | SUM(CASE WHEN minor_version = '2.16' THEN total_downloads ELSE 0 END) AS "2.16", 44 | SUM(CASE WHEN minor_version = '3.0' THEN total_downloads ELSE 0 END) AS "3.0", 45 | SUM(CASE WHEN minor_version = '4.0' THEN total_downloads ELSE 0 END) AS "4.0", 46 | SUM(CASE WHEN minor_version = '4.1' THEN total_downloads ELSE 0 END) AS "4.1", 47 | SUM(CASE WHEN minor_version = '4.2' THEN total_downloads ELSE 0 END) AS "4.2", 48 | SUM(CASE WHEN minor_version = '5.0' THEN total_downloads ELSE 0 END) AS "5.0", 49 | SUM(CASE WHEN minor_version = '5.1' THEN total_downloads ELSE 0 END) AS "5.1", 50 | SUM(CASE WHEN minor_version = '5.2' THEN total_downloads ELSE 0 END) AS "5.2", 51 | SUM(CASE WHEN minor_version = '6.0' THEN total_downloads ELSE 0 END) AS "6.0", 52 | SUM(CASE WHEN minor_version = '6.1' THEN total_downloads ELSE 0 END) AS "6.1", 53 | SUM(CASE WHEN minor_version = '6.2' THEN total_downloads ELSE 0 END) AS "6.2", 54 | SUM(CASE WHEN minor_version = '6.3' THEN total_downloads ELSE 0 END) AS "6.3", 55 | SUM(CASE WHEN minor_version = '6.4' THEN total_downloads ELSE 0 END) AS "6.4", 56 | FROM feature_versions 57 | GROUP BY day 58 | ORDER BY day; 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wagtail tooling 2 | 3 | > Tools for Wagtail core development, with a particular focus on UI. 4 | 5 | This project contains a test suite for Wagtail, which generates: 6 | 7 | - Visual regression testing cases for BackstopJS. 8 | - Accessibility checks with Pa11y. 9 | - A spreadsheet export to help with UI audits in Google Sheets. 10 | - A Figma-friendly spreadsheet export to help with audits of UI screenshots. 11 | 12 | ## Installation 13 | 14 | ```sh 15 | # Get the code from the repository. 16 | git clone git@github.com:thibaudcolas/wagtail-tooling.git 17 | cd wagtail-tooling 18 | # Install dependencies. 19 | nvm install 20 | npm install 21 | # Configure environment variables. 22 | touch .env 23 | # Configure Wagtail user session ID to use. 24 | # Get this value by logging into the Wagtail admin of your site, then 25 | # use the developer tools to insect the cookies, to find "sessionid". 26 | # echo "WAGTAIL_SESSIONID=yoursessionid" >> .env 27 | ``` 28 | 29 | You will also need to update the `loadSVG.js` file to contain an up-to-date copy of Wagtail’s icons sprite. 30 | 31 | ## Usage 32 | 33 | ### UI regression tests 34 | 35 | ```sh 36 | # 1. Run UI regression tests. 37 | npm run regression:test 38 | # 2. Create UI regression reference. 39 | npm run regression:approve 40 | # 3. Open UI regression report. 41 | npm run regression:open 42 | ``` 43 | 44 | ### Documentation screenshots 45 | 46 | We use `wagtail-tooling` to keep Wagtail’s documentation screenshots up-to-date. Using this tooling helps to: 47 | 48 | 1. Make sure screenshots use consistent sizes and uniform highlights 49 | 2. Automate the screenshot taking process 50 | 3. Know what screenshots do need updating, based on visual regression testing results 51 | 52 | #### Demo sites 53 | 54 | - [wagtail-tutorial-site](https://github.com/thibaudcolas/wagtail-tutorial-site) is Wagtail’s official getting started tutorial, to keep the tutorial screenshots up-to-date. 55 | - [bakerydemo-editor-guide](https://github.com/thibaudcolas/bakerydemo-editor-guide) is an extension of Wagtail’s [bakerydemo](https://github.com/wagtail/bakerydemo) with additional content relevant for [guide.wagtail.org](https://guide.wagtail.org/en-latest/) and [docs.wagtail.org](https://docs.wagtail.org/). 56 | 57 | #### Screenshot scenarios 58 | 59 | See [`docs-screenshots/backstop.config.js`](docs-screenshots/backstop.config.js). There are three sets of scenarios which need toggling on and off with code comments in the `scenarios` array: 60 | 61 | - docs.wagtail.org: tutorial 62 | - docs.wagtail.org: extending admin views 63 | - guide.wagtail.org 64 | 65 | #### Commands 66 | 67 | ```sh 68 | # 1. Run UI regression tests. 69 | npm run docs:regression:test 70 | # 2. Create UI regression reference. 71 | npm run docs:regression:approve 72 | # 3. Open UI regression report. 73 | npm run docs:regression:open 74 | ``` 75 | 76 | #### Updating screenshots 77 | 78 | After the regression testing is done, optimize images with [ImageOptim](https://imageoptim.com/mac) or equivalent, make a pull request to Wagtail, and follow [guide.wagtail.org Images docs](https://guide.wagtail.org/en-latest/contributing/#images). 79 | 80 | Store the optimized images in [wagtail-tooling-screenshots](https://github.com/thibaudcolas/wagtail-tooling-screenshots). Though not a must, this helps with future screenshot taking runs so we can identify the screenshots that have actually changed and require updating. 81 | 82 | ## Examples 83 | 84 | ### Visual regression 85 | 86 | - Visual regression testing report: [2022-06-29 sample Wagtail 4.0dev Backstop.js report](https://wagtail-tooling-sample-reports.netlify.app/20220629-backstop_sample_report/html_report/index.html) 87 | - Contrast themes screenshots: [2022-06-29 Wagtail 4.0 light high contrast screenshots](https://wagtail-tooling-sample-reports.netlify.app/20220629-contrast-sample/html_report/index.html) 88 | 89 | ### Accessibility tests 90 | 91 | - Accessibility checks report: [2022-06-30 Pa11y + Lighthouse report](https://wagtail-tooling-sample-reports.netlify.app/20220630-pa11y/index.html) 92 | 93 | ### Reports 94 | 95 | - Spreadsheet export: [Wagtail | 3.0 → 4.0 UI overview – Views](https://docs.google.com/spreadsheets/d/1WaqARpHf99U0O94hypwHNjHA9yNpn4AnXnIT5AdJsm8/edit#gid=1962441802) 96 | - Figma-friendly spreadsheet export: [3.0 → 4.0 Figma sync sheet](https://docs.google.com/spreadsheets/d/1WaqARpHf99U0O94hypwHNjHA9yNpn4AnXnIT5AdJsm8/edit#gid=414045255) 97 | - Manually-created Figma UI inventory: [Figma – Wagtail 3 UI inventory – Views](https://www.figma.com/file/3SZAkXYKTo52047weXDvb9/Wagtail-3-UI-Inventory?node-id=6609%3A37945) 98 | 99 | ## References 100 | 101 | - 102 | - 103 | - 104 | - 105 | -------------------------------------------------------------------------------- /http-archive/aria-label/main.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const cheerio = require("cheerio"); 4 | const { roles, elementRoles } = require("aria-query"); 5 | 6 | const folderPath = "../html"; 7 | 8 | const outputFile = "aria-label-patterns.csv"; 9 | 10 | const header = 11 | [ 12 | "role", 13 | "tagName", 14 | "role allows", 15 | "ariaLabelStartsWithVisibleText", 16 | "visibleTextInAriaLabel", 17 | "aria-label", 18 | "innerText", 19 | "startTag", 20 | ] 21 | .map((col) => `"${col}"`) 22 | .join(",") + "\n"; 23 | 24 | fs.writeFileSync(outputFile, header); 25 | 26 | function escapeCSV(text) { 27 | return `"${String(text).replace(/"/g, '""')}"`; 28 | } 29 | 30 | function getImplicitRole(elem) { 31 | const tagName = elem.name; 32 | const attribs = elem.attribs || {}; 33 | 34 | for (const [elementDef, roles] of elementRoles.entries()) { 35 | if (elementDef.name === tagName) { 36 | let matches = true; 37 | if (elementDef.attributes && elementDef.attributes.size > 0) { 38 | for (const [attrName, attrValues] of elementDef.attributes.entries()) { 39 | // If the element doesn't have the attribute or its value isn't in the allowed set, it's not a match. 40 | if (!attribs[attrName] || !attrValues.has(attribs[attrName])) { 41 | matches = false; 42 | break; 43 | } 44 | } 45 | } 46 | 47 | // Refine the match when the tag name maps to different roles based on attributes. 48 | // See https://www.w3.org/TR/html-aam-1.0/. 49 | if (matches) { 50 | if (elementDef.name === "a" && attribs.href) { 51 | return "link"; 52 | } 53 | 54 | if (elementDef.name === "input" && attribs.type) { 55 | if (attribs.type === "checkbox" || attribs.type === "radio") { 56 | return attribs.type; 57 | } else if (attribs.type === "button" || attribs.type === "submit") { 58 | return "button"; 59 | } else if (attribs.type === "text" || attribs.type === "password") { 60 | return "textbox"; 61 | } else if (attribs.type === "range") { 62 | return "slider"; 63 | } else if (attribs.type === "search") { 64 | return "searchbox"; 65 | } 66 | } 67 | 68 | // Return the roles as a comma-separated string. 69 | return Array.from(roles).join(", "); 70 | } 71 | } 72 | } 73 | return ""; 74 | } 75 | 76 | function processFile(filePath) { 77 | fs.readFile(filePath, "utf8", (err, data) => { 78 | if (err) { 79 | console.error("Error reading file:", filePath, err); 80 | return; 81 | } 82 | const $ = cheerio.load(data); 83 | 84 | $("[aria-label]").each((i, elem) => { 85 | let role = $(elem).attr("role") || ""; 86 | if (!role) { 87 | role = getImplicitRole(elem); 88 | } 89 | 90 | const ariaLabel = $(elem).attr("aria-label").replace(/\n/g, " ") || ""; 91 | const tagName = elem.tagName ? elem.tagName.toLowerCase() : ""; 92 | const startTagHTML = ($.html(elem).split(">")[0] + ">").replace( 93 | /\n/g, 94 | " " 95 | ); 96 | // This data is normally in aria-query but that data doesn’t reflect actual behavior of browsers / assistive tech. 97 | // List from https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-label#associated_roles. 98 | const ariaLabelUnsupported = [ 99 | "code", 100 | "caption", 101 | "definition", 102 | "deletion", 103 | "emphasis", 104 | "generic", 105 | "insertion", 106 | "mark", 107 | "paragraph", 108 | "presentation", 109 | "none", 110 | "strong", 111 | "subscript", 112 | "superscript", 113 | "suggestion", 114 | "term", 115 | "time", 116 | ].includes(role); 117 | const roleAllowsAriaLabel = !ariaLabelUnsupported; 118 | 119 | let innerText = $(elem).text().replace(/\n/g, " ").trim() || ""; 120 | 121 | const isLandmark = [ 122 | "banner", 123 | "complementary", 124 | "contentinfo", 125 | "form", 126 | "main", 127 | "navigation", 128 | "region", 129 | "search", 130 | ].includes(role); 131 | // Treat landmarks as not having text, so usage of aria-label with landmarks is always considered "correct" in the stats above. 132 | // We want to report issues based on overall number of aria-label occurrences, including landmarks in the total. 133 | innerText = isLandmark ? "" : innerText; 134 | 135 | const ariaLabelStartsWithVisibleText = ariaLabel.startsWith(innerText); 136 | const visibleTextInAriaLabel = ariaLabel.includes(innerText); 137 | 138 | const line = 139 | [ 140 | escapeCSV(role), 141 | escapeCSV(tagName), 142 | escapeCSV(roleAllowsAriaLabel ? "Yes" : "No"), 143 | escapeCSV(ariaLabelStartsWithVisibleText ? "Yes" : "No"), 144 | escapeCSV(visibleTextInAriaLabel ? "Yes" : "No"), 145 | escapeCSV(ariaLabel), 146 | escapeCSV(innerText), 147 | escapeCSV(startTagHTML), 148 | ].join(",") + "\n"; 149 | 150 | fs.appendFileSync(outputFile, line); 151 | }); 152 | }); 153 | } 154 | 155 | function scanDirectory(directoryPath) { 156 | fs.readdir(directoryPath, { withFileTypes: true }, (err, entries) => { 157 | if (err) { 158 | console.error("Error reading directory:", directoryPath, err); 159 | return; 160 | } 161 | 162 | entries.forEach((entry) => { 163 | const entryPath = path.join(directoryPath, entry.name); 164 | 165 | if (entry.isFile() && entry.name.endsWith(".html")) { 166 | processFile(entryPath); 167 | } else if (entry.isDirectory()) { 168 | // Scan the subdirectory (one level deep). 169 | fs.readdir(entryPath, { withFileTypes: true }, (err, subEntries) => { 170 | if (err) { 171 | console.error("Error reading subdirectory:", entryPath, err); 172 | return; 173 | } 174 | subEntries.forEach((subEntry) => { 175 | if (subEntry.isFile() && subEntry.name.endsWith(".html")) { 176 | const subEntryPath = path.join(entryPath, subEntry.name); 177 | processFile(subEntryPath); 178 | } 179 | }); 180 | }); 181 | } 182 | }); 183 | }); 184 | } 185 | 186 | scanDirectory(folderPath); 187 | -------------------------------------------------------------------------------- /accessibility/old/index.test.js: -------------------------------------------------------------------------------- 1 | /* global page */ 2 | const { toHaveNoViolations } = require("jest-axe"); 3 | const AxeReports = require("axe-reports"); 4 | 5 | // const scenarios = require('../backstop/scenarios'); 6 | 7 | expect.extend(toHaveNoViolations); 8 | 9 | const views = [ 10 | "/", 11 | "/404", 12 | "/styleguide/", 13 | "/pages/preview", 14 | "/pages/", 15 | "/pages/search/?q=bread", 16 | "/pages/60/", 17 | 18 | "/pages/60/?ordering=ord", 19 | "/pages/60/edit/", 20 | // '/pages/60/revisions/', 21 | // '/pages/60/unpublish/', 22 | // '/pages/60/delete/', 23 | // '/pages/60/copy/', 24 | // '/pages/60/add_subpage/', 25 | 26 | // '/pages/add/base/homepage/60/', 27 | // '/pages/69/move/60/', 28 | "/base/people/", 29 | "/base/people/edit/1/", 30 | "/base/people/delete/1/", 31 | "/base/people/create/", 32 | "/images/", 33 | 34 | "/images/?q=bread", 35 | "/images/?collection_id=2", 36 | "/images/47/", 37 | "/images/47/delete/", 38 | "/images/add/", 39 | "/documents/", 40 | "/documents/?collection_id=2", 41 | 42 | "/documents/multiple/add/", 43 | "/snippets/", 44 | "/snippets/base/people/", 45 | "/snippets/base/people/1/", 46 | "/snippets/base/people/1/delete/", 47 | "/snippets/base/people/add/", 48 | "/forms/", 49 | "/forms/submissions/69/", 50 | "/users/", 51 | "/users/3/", 52 | "/users/add/", 53 | "/groups/", 54 | "/groups/?q=edi", 55 | "/groups/1/", 56 | "/groups/1/delete/", 57 | "/groups/new/", 58 | "/sites/", 59 | "/sites/2/", 60 | "/sites/2/delete/", 61 | "/sites/new/", 62 | "/collections/", 63 | "/collections/2/", 64 | "/collections/2/delete/", 65 | "/collections/add/", 66 | "/redirects/", 67 | "/redirects/?q=test", 68 | "/redirects/add/", 69 | "/searchpicks/", 70 | "/searchpicks/?q=test", 71 | "/searchpicks/add/", 72 | "/account/", 73 | "/account/change_password/", 74 | "/account/notification_preferences/", 75 | "/account/language_preferences/", 76 | "/forms/submissions/69/?date_from=2017-01-01&date_to=2050-01-01&action=filter", 77 | "/users/?q=admin", 78 | "/password_reset/", 79 | ]; 80 | 81 | describe("Accessibility", () => { 82 | beforeAll(async () => { 83 | // await page.setCookie({ 84 | // domain: 'localhost', 85 | // path: '/', 86 | // name: 'sessionid', 87 | // value: 'grdhyy5v829zi6h8hdyoib3cfb8fm18d', 88 | // expirationDate: 1798790400, 89 | // hostOnly: false, 90 | // httpOnly: false, 91 | // secure: false, 92 | // session: false, 93 | // sameSite: 'no_restriction', 94 | // }); 95 | // await page.goto('http://localhost:8000/admin'); 96 | // await page.addScriptTag({ path: require.resolve('axe-core') }); 97 | }); 98 | 99 | views.forEach((path) => { 100 | it(path, async () => { 101 | console.log(path); 102 | jest.setTimeout(20000); 103 | 104 | // page = await browser.newPage(); 105 | await page.setCookie({ 106 | domain: "localhost", 107 | path: "/", 108 | name: "sessionid", 109 | value: "grdhyy5v829zi6h8hdyoib3cfb8fm18d", 110 | expirationDate: 1798790400, 111 | hostOnly: false, 112 | httpOnly: false, 113 | secure: false, 114 | session: false, 115 | sameSite: "no_restriction", 116 | }); 117 | await page.goto(`http://localhost:8000/admin${path}`); 118 | await page.addScriptTag({ path: require.resolve("axe-core") }); 119 | 120 | const result = await page.evaluate( 121 | () => 122 | new Promise((resolve) => { 123 | window.axe.run((err, results) => { 124 | if (err) throw err; 125 | resolve(results); 126 | }); 127 | }), 128 | ); 129 | AxeReports.processResults( 130 | result, 131 | "csv", 132 | `accessibility/${path.replace(/\//g, "-")}`, 133 | true, 134 | ); 135 | expect(result).toHaveNoViolations(); 136 | }); 137 | }); 138 | 139 | // test.each` 140 | // id | path 141 | // ${1} | ${'/'} 142 | // ${2} | ${'/404'} 143 | // ${3} | ${'/styleguide/'} 144 | // ${4} | ${'/pages/preview'} 145 | // ${5} | ${'/pages/'} 146 | // ${6} | ${'/pages/search/?q=bread'} 147 | // ${8} | ${'/pages/60/'} 148 | // ${9} | ${'/pages/60/?ordering=ord'} 149 | // ${10} | ${'/pages/60/edit/'} 150 | // ${11} | ${'/pages/60/revisions/'} 151 | // ${12} | ${'/pages/60/unpublish/'} 152 | // ${13} | ${'/pages/60/delete/'} 153 | // ${14} | ${'/pages/60/copy/'} 154 | // ${15} | ${'/pages/60/add_subpage/'} 155 | // ${16} | ${'/pages/add/base/homepage/60/'} 156 | // ${17} | ${'/pages/69/move/60/'} 157 | // ${18} | ${'/base/people/'} 158 | // ${19} | ${'/base/people/edit/1/'} 159 | // ${20} | ${'/base/people/delete/1/'} 160 | // ${21} | ${'/base/people/create/'} 161 | // ${22} | ${'/images/'} 162 | // ${23} | ${'/images/?q=bread'} 163 | // ${24} | ${'/images/?collection_id=2'} 164 | // ${25} | ${'/images/47/'} 165 | // ${26} | ${'/images/47/delete/'} 166 | // ${27} | ${'/images/add/'} 167 | // ${28} | ${'/documents/'} 168 | // ${29} | ${'/documents/?collection_id=2'} 169 | // ${30} | ${'/documents/multiple/add/'} 170 | // ${31} | ${'/snippets/'} 171 | // ${32} | ${'/snippets/base/people/'} 172 | // ${33} | ${'/snippets/base/people/1/'} 173 | // ${34} | ${'/snippets/base/people/1/delete/'} 174 | // ${35} | ${'/snippets/base/people/add/'} 175 | // ${36} | ${'/forms/'} 176 | // ${37} | ${'/forms/submissions/69/'} 177 | // ${38} | ${'/users/'} 178 | // ${39} | ${'/users/3/'} 179 | // ${40} | ${'/users/add/'} 180 | // ${41} | ${'/groups/'} 181 | // ${42} | ${'/groups/?q=edi'} 182 | // ${43} | ${'/groups/1/'} 183 | // ${44} | ${'/groups/1/delete/'} 184 | // ${45} | ${'/groups/new/'} 185 | // ${46} | ${'/sites/'} 186 | // ${47} | ${'/sites/2/'} 187 | // ${48} | ${'/sites/2/delete/'} 188 | // ${49} | ${'/sites/new/'} 189 | // ${50} | ${'/collections/'} 190 | // ${51} | ${'/collections/2/'} 191 | // ${52} | ${'/collections/2/delete/'} 192 | // ${53} | ${'/collections/add/'} 193 | // ${54} | ${'/redirects/'} 194 | // ${55} | ${'/redirects/?q=test'} 195 | // ${56} | ${'/redirects/add/'} 196 | // ${57} | ${'/searchpicks/'} 197 | // ${58} | ${'/searchpicks/?q=test'} 198 | // ${59} | ${'/searchpicks/add/'} 199 | // ${60} | ${'/account/'} 200 | // ${61} | ${'/account/change_password/'} 201 | // ${62} | ${'/account/notification_preferences/'} 202 | // ${63} | ${'/account/language_preferences/'} 203 | // ${64} | ${'/forms/submissions/69/?date_from=2017-01-01&date_to=2050-01-01&action=filter'} 204 | // ${65} | ${'/users/?q=admin'} 205 | // ${66} | ${'/password_reset/'} 206 | // `('a11y', async ({ path }) => { 207 | // console.log(path); 208 | // jest.setTimeout(20000); 209 | 210 | // await page.setCookie({ 211 | // domain: 'localhost', 212 | // path: '/', 213 | // name: 'sessionid', 214 | // value: 'grdhyy5v829zi6h8hdyoib3cfb8fm18d', 215 | // expirationDate: 1798790400, 216 | // hostOnly: false, 217 | // httpOnly: false, 218 | // secure: false, 219 | // session: false, 220 | // sameSite: 'no_restriction', 221 | // }); 222 | // await page.goto(`http://localhost:8000/admin${path}`); 223 | // await page.addScriptTag({ path: require.resolve('axe-core') }); 224 | 225 | // const result = await page.evaluate( 226 | // () => 227 | // new Promise(resolve => { 228 | // window.axe.run((err, results) => { 229 | // if (err) throw err; 230 | // resolve(results); 231 | // }); 232 | // }), 233 | // ); 234 | 235 | // AxeReports.processResults( 236 | // result, 237 | // 'csv', 238 | // `accessibility/${path.replace(/\//g, '')}`, 239 | // true, 240 | // ); 241 | // expect(result).toHaveNoViolations(); 242 | // }); 243 | }); 244 | -------------------------------------------------------------------------------- /accessibility/pa11y-test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const pa11y = require("pa11y"); 3 | const puppeteer = require("puppeteer"); 4 | const lighthouse = require("lighthouse"); 5 | 6 | require("dotenv").config(); 7 | 8 | const scenarios = require("../ui/scenarios"); 9 | 10 | const WAGTAIL_SESSIONID = process.env.WAGTAIL_SESSIONID; 11 | 12 | const ADMIN_ROOT = "http://localhost:8000/admin"; 13 | 14 | let views = []; 15 | 16 | const hasOnlyFlag = (s) => 17 | s.only || (s.states && s.states.find((state) => state.only)); 18 | 19 | const HAS_ONLY_FILTER = scenarios.find(hasOnlyFlag); 20 | 21 | scenarios.forEach((scenario) => { 22 | const states = scenario.states || []; 23 | 24 | views.push(scenario); 25 | 26 | states 27 | .filter((s) => typeof s === "object") 28 | .forEach((state) => { 29 | views.push({ 30 | ...scenario, 31 | ...state, 32 | label: `${scenario.label} - ${state.label}`, 33 | // emulateVisionDeficiency: "achromatopsia", 34 | // emulateMediaFeatures: [ 35 | // { name: "forced-colors", value: "active" }, 36 | // { name: "prefers-contrast", value: "more" }, 37 | // ], 38 | }); 39 | }); 40 | }); 41 | 42 | const shouldTest = (s) => { 43 | if (HAS_ONLY_FILTER) { 44 | return s.only; 45 | } 46 | 47 | return !s.skip; 48 | }; 49 | 50 | views = views.filter(shouldTest); 51 | 52 | const getAuthCookie = async (browser) => { 53 | // let page = await browser.newPage(); 54 | // await page.deleteCookie({ 55 | // name: "sessionid", 56 | // domain: "localhost", 57 | // path: "/", 58 | // }); 59 | // await page.goto(`${ADMIN_ROOT}/login`); 60 | // await page.type("#id_username", "admin"); 61 | // await page.type("#id_password", "changeme"); 62 | // await page.keyboard.press("Enter"); 63 | // await page.waitForSelector(".page404__header"); 64 | // const sessionid = await page.evaluate(() => { 65 | // const cookieMatch = document.cookie.match(/sessionid=([^;]+)/); 66 | // return cookieMatch ? cookieMatch[1] : "error"; 67 | // }); 68 | 69 | // // We will be setting the cookie manually per-page. Do not want it to be stored for the whole browser. 70 | // await page.deleteCookie({ 71 | // name: "sessionid", 72 | // domain: "localhost", 73 | // path: "/", 74 | // }); 75 | 76 | return { 77 | name: "sessionid", 78 | domain: "localhost", 79 | path: "/", 80 | value: WAGTAIL_SESSIONID, 81 | expirationDate: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 30, // 30 days 82 | hostOnly: false, 83 | httpOnly: false, 84 | secure: false, 85 | session: false, 86 | sameSite: "no_restriction", 87 | }; 88 | }; 89 | 90 | const run = async () => { 91 | const browser = await puppeteer.launch(); 92 | 93 | try { 94 | let issues = []; 95 | 96 | const sharedCookie = await getAuthCookie(browser); 97 | 98 | for (const scenario of views) { 99 | console.log(scenario.label, scenario.path); 100 | 101 | const page = await browser.newPage(); 102 | 103 | const headers = { 104 | Cookie: `sessionid=${sharedCookie.value};`, 105 | }; 106 | 107 | await page.setRequestInterception(true); 108 | page.removeAllListeners("request"); 109 | let interceptionHandled = false; 110 | page.on("request", (request) => { 111 | // This is normally implemented by pa11y. We do this ourselves because it would be incompatible with any other request override. 112 | const overrides = {}; 113 | if (!scenario.unauthenticated) { 114 | if (!interceptionHandled) { 115 | overrides.headers = {}; 116 | for (const [key, value] of Object.entries(headers)) { 117 | overrides.headers[key.toLowerCase()] = value; 118 | } 119 | 120 | interceptionHandled = true; 121 | } 122 | } 123 | 124 | if (scenario.requestOverrides) { 125 | const overrideKey = Object.keys(scenario.requestOverrides).find( 126 | (path) => request.url().includes(path), 127 | ); 128 | const override = scenario.requestOverrides[overrideKey]; 129 | 130 | if (override) { 131 | if (typeof override === "object") { 132 | request.respond(override); 133 | } else { 134 | setTimeout(() => { 135 | request.continue(); 136 | }, override); 137 | } 138 | return; 139 | } 140 | } 141 | 142 | request.continue(overrides); 143 | }); 144 | 145 | await page.deleteCookie({ 146 | name: "sessionid", 147 | domain: "localhost", 148 | path: "/", 149 | }); 150 | 151 | if (!scenario.unauthenticated) { 152 | if (sharedCookie === "error") { 153 | throw new Error( 154 | "sessionid cookie is unreadable. Did you set up SESSION_COOKIE_HTTPONLY = False in Django settings?", 155 | ); 156 | } 157 | await page.setCookie(sharedCookie); 158 | } 159 | 160 | if (scenario.emulateVisionDeficiency) { 161 | await page.emulateVisionDeficiency(scenario.emulateVisionDeficiency); 162 | } 163 | 164 | if (scenario.emulateMediaFeatures) { 165 | const client = await page.target().createCDPSession(); 166 | await client.send("Emulation.setEmulatedMedia", { 167 | features: scenario.emulateMediaFeatures, 168 | }); 169 | } 170 | 171 | const fullLabel = `${scenario.category} – ${scenario.label}`; 172 | 173 | const pa11yOptions = { 174 | standard: "WCAG2AAA", 175 | log: { 176 | debug: console.log, 177 | error: console.error, 178 | info: console.log, 179 | }, 180 | runners: ["axe", "htmlcs"], 181 | ignore: [ 182 | // The heading structure is not logically nested. 183 | "WCAG2AAA.Principle1.Guideline1_3.1_3_1_AAA.G141", 184 | // This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 7:1. 185 | "WCAG2AAA.Principle1.Guideline1_4.1_4_6.G17.Fail", 186 | ], 187 | actions: scenario.actions || [], 188 | viewport: scenario.viewport || { 189 | width: 1024, 190 | height: 768, 191 | deviceScaleFactor: 1, 192 | isMobile: false, 193 | }, 194 | screenCapture: `${__dirname}/data/screenshots/${fullLabel}.png`, 195 | browser, 196 | page, 197 | }; 198 | const result = await pa11y(`${ADMIN_ROOT}${scenario.path}`, pa11yOptions); 199 | 200 | if (HAS_ONLY_FILTER) { 201 | console.log(result); 202 | } 203 | 204 | issues = issues.concat( 205 | result.issues.map((issue) => { 206 | return { 207 | label: fullLabel, 208 | documentTitle: result.documentTitle, 209 | pageUrl: result.pageUrl, 210 | code: issue.code, 211 | context: issue.context, 212 | message: issue.message, 213 | type: issue.type, 214 | selector: issue.selector, 215 | runner: issue.runner, 216 | screenshot: `${fullLabel}.png`, 217 | lighthouseReport: `${fullLabel}.html`, 218 | }; 219 | }), 220 | ); 221 | 222 | fs.writeFileSync(`./pa11y.json`, JSON.stringify(issues, null, 2), "utf8"); 223 | 224 | const runnerResult = await lighthouse(`${ADMIN_ROOT}${scenario.path}`, { 225 | port: new URL(browser.wsEndpoint()).port, 226 | onlyCategories: ["accessibility", "best-practices", "seo"], 227 | // onlyCategories: ["accessibility", "best-practices", "seo", "pwa", "performance"], 228 | output: "html", 229 | // logLevel: "info", 230 | }); 231 | const reportHtml = runnerResult.report; 232 | fs.writeFileSync( 233 | `${__dirname}/data/lighthouse/${fullLabel}.html`, 234 | reportHtml, 235 | ); 236 | } 237 | } catch (error) { 238 | // Output an error if it occurred 239 | console.error(error.message); 240 | } 241 | 242 | browser.close(); 243 | }; 244 | 245 | run(); 246 | -------------------------------------------------------------------------------- /ui/docs/ui-overview.tsv: -------------------------------------------------------------------------------- 1 | Category View Admin path (example) UI states Relevance Review comments UI notes 2 | 3 | 4 | Dashboard 5 | Dashboard / Wagtail upgrade, Most recent edits, Pages awaiting moderation 6 | Authentication 7 | Login /login/ Validation error 8 | Logout /logout “You have been successfully logged out.” message on Login screen 9 | Password reset /password_reset/ Validation error 10 | Password reset done /password_reset/done/ 11 | Navigation 12 | Search form / 13 | Account menu / 14 | Settings menu / 15 | ModelAdmin menu / 16 | Pages menu / Loading, Server error 17 | Pages menu level 2 / Loading, Server error 18 | Mobile menu toggle / 19 | Edit bird /edit-bird Active 20 | No JS / “Javascript is required to use Wagtail, but it is currently disabled.” banner at the top of all pages 21 | Page not found (404) /404 22 | Unauthorised access (403) / “Sorry, you do not have permission to access this area.” message at the top of the dasboard 23 | Pages 24 | View child pages /pages/60/ Empty, Reorder child pages, Sort by , Pagination, Root level 25 | Search /pages/search/?q=bread No results, Page type filter, Sort by , Pagination 26 | Set privacy /pages/60/ Validation error 27 | View all revisions /pages/60/revisions/ Sort by , Pagination 28 | Compare revisions /pages/60/revisions/compare/32...34/ Empty 29 | Preview revision /pages/60/revisions/37/view/ This serves the same view as the “live” page, but with different content. This should at least change the page title to make it clear it's a revision 30 | Review revision /pages/60/revisions/37/revert/ Success Uses the standard page editing UI, but with a top banner and different footer 31 | Unpublish /pages/60/unpublish/ Success 32 | Delete /pages/60/delete/ Success 33 | Copy /pages/60/copy/ Validation error, Success 34 | Move /pages/69/move/60/ Pagination, No move target, Confirm, Success 35 | Edit lock /pages/60/edit/ Locked, Unlocked 36 | Add child page /pages/60/add_subpage/ 37 | Create /pages/add/base/homepage/60/ Validation error, Success 38 | Edit /pages/60/edit/ 39 | Edit promote tab /pages/60/edit/#tab-promote 40 | Edit settings tab /pages/60/edit/#tab-settings 41 | Preview /pages/60/edit/preview/ This serves the same view as the “live” page, but with different content. This should at least change the page title to make it clear it's a revision 42 | Rich text (wagtail-markdown) 43 | Text formats Bold, Italic, Heading levels, Bullet list, Numbered list 44 | Blocks Horizontal rule, images, tables 45 | Links Links 46 | Other controls Preview, Undo, Redo 47 | Rich text (Draftail) 48 | Text formats Bold, Italic, Heading levels, Bullet list, Numbered list 49 | Blocks Horizontal rule, Embed, Image, Blocks tooltip 50 | Inlines Links, Documents, Inlines tooltip 51 | Other controls Line break, Undo, Redo 52 | Rich text (Hallo) 53 | Text formats Bold, Italic, Heading levels, Bullet list, Numbered list 54 | Blocks Horizontal rule, images, tables 55 | Inlines Links 56 | Other controls Preview, Undo, Redo 57 | Choosers 58 | Image chooser /pages/60/edit/ Loading, Pagination, Collections filter, Search 59 | Images chooser upload /pages/60/edit/ Uploading, Validation error 60 | Embed chooser /pages/60/edit/ Loading, Validation error, Uploading 61 | Document chooser /pages/60/edit/ Loading, Pagination, Collections filter, Search 62 | Link chooser /pages/60/edit/ Loading, Pagination, Search, Explorer navigation, Preselected page 63 | Link chooser – external link /pages/60/edit/ Validation error 64 | Email link chooser /pages/60/edit/ Validation error 65 | Page chooser /pages/60/edit/ Loading, Pagination, Search, Explorer navigation, Preselected page 66 | StreamField 67 | StreamField /pages/60/edit/ Not detailed to an accurate level because of the work involved 68 | Add block /pages/60/edit/ 69 | Move block /pages/60/edit/ 70 | Delete block /pages/60/edit/ 71 | Drag and drop block /pages/60/edit/ 72 | Copy block /pages/60/edit/ 73 | Nested StreamField /pages/60/edit/ 74 | TableBlock /pages/60/edit/ Not broken down because of the work involved 75 | Panels 76 | InlinePanel /pages/60/edit/ Add, Move, Delete 77 | MultiFieldPanel /pages/60/edit/ 78 | ModelAdmin 79 | View all /base/people/ Empty, Pagination, Sort by 80 | Edit /base/people/edit/1/ Validation error, Success 81 | Add /base/people/create/ Validation error, Success 82 | Delete /base/people/delete/1/ Success 83 | Images 84 | View all /images/ Empty, Pagination 85 | Search /images/?q=bread No results, Pagination 86 | Collections filter /images/?collection_id=2 No results, Pagination 87 | Edit /images/47/ File not found error on load, Validation error, Focal point set, Success 88 | Add an image /images/add/ File not found error on load, Validation error, Focal point set, Success 89 | Delete /images/47/delete/ Success 90 | Image URL generator /images/47/generate_url/ Focal point set 91 | Media 92 | View all /media/ Empty, Pagination, Sort by Third-party wagtailmedia extension. Not part of Wagtail core 93 | Search /media/?q=bread No results, Pagination 94 | Edit /media/edit/1/ Validation error, Success 95 | Add audio /media/audio/add/ Validation error, Uploading, Success 96 | Add video /media/video/add/ Validation error, Uploading, Success 97 | Delete /images/delete/1/ Success 98 | Documents 99 | View all /documents/ Empty, Pagination, Sort by 100 | Search /documents/?q=wagtail No results, Pagination 101 | Collections filter /documents/?collection_id=2 No results, Pagination 102 | Edit /documents/edit/1/ File not found error on load, Validation error, Success 103 | Add a document /documents/multiple/add/ Loading, Validation error, Success 104 | Delete /documents/delete/1/ Success 105 | Snippets 106 | View all types /snippets/ Empty 107 | View all /snippets/base/people/ Empty, Pagination, Delete selected 108 | Search snippets /snippets/base/people/?q=test No results 109 | Edit /snippets/base/people/1/ Validation error, Success 110 | Add /snippets/base/people/add/ Validation error, Success 111 | Delete /snippets/base/people/1/delete/ Success 112 | Forms 113 | View all /forms/ Empty, Pagination 114 | View submissions /forms/submissions/69/ Sort by , Delete selected 115 | Submissions date picker /forms/submissions/69/ 116 | Submissions date range /forms/submissions/69/?date_from=2017-01-01&date_to=2050-01-01&action=filter No results 117 | Submissions CSV download /forms/submissions/69/ 118 | Users 119 | View all /users/ Sort by , Pagination 120 | Search /users/?q=admin No results, Pagination 121 | Edit /users/3/ Validation error, Success 122 | Edit roles /users/3/#roles 123 | Add /users/add/ Validation error, Success 124 | Add roles /users/add/#roles 125 | Delete /users/4/delete/ Success 126 | Groups 127 | View all /groups/ Empty, Pagination 128 | Search /groups/?q=edi No results, Pagination 129 | Edit /groups/1/ Validation error, Success 130 | Add a group /groups/new/ Validation error, Success 131 | Add permissions /groups/1/ 132 | Delete permissions /groups/1/ 133 | Delete /groups/1/delete/ Success 134 | Sites 135 | View all /sites/ Sort by 136 | Edit /sites/2/ Validation error, Success 137 | Add a site /sites/new/ Validation error, Success 138 | Delete /sites/2/delete/ Success 139 | Collections 140 | View all /collections/ Empty 141 | Edit /collections/2/ Validation error, Success 142 | Set privacy /collections/2/ Validation error, Success 143 | Add a collection /collections/add/ Validation error, Success 144 | Delete /collections/2/delete/ Success 145 | Redirects 146 | View all /redirects/ Empty, Sort by , Pagination 147 | Search /redirects/?q=test No results, Pagination 148 | Edit /redirects/1/ Validation error, Success 149 | Add /redirects/add/ 150 | Add / edit pages chooser /redirects/add/ Loading, Search, Search no results, Explorer navigation 151 | Promoted search 152 | View all /searchpicks/ Empty, Pagination 153 | Search /searchpicks/?q=test No results, Pagination 154 | Edit /searchpicks/5/ Validation error, Success 155 | Add / Edit search term chooser /searchpicks/5/ Loading, Search, Search no results, Pagination 156 | Add / edit pages chooser /searchpicks/5/ Loading, Search, Search no results, Explorer navigation 157 | Add results /searchpicks/add/ Validation error, Success 158 | Delete /searchpicks/5/delete/ Success 159 | Styleguide 160 | Styleguide /styleguide/ 161 | Site settings 162 | Site settings / 163 | User account 164 | Account actions /account/ 165 | Change profile picture /account/change_avatar/ Validation error 166 | Change email /account/change_email/ Validation error, Success 167 | Change password /account/change_password/ Validation error, Success 168 | Notification preferences /account/notification_preferences/ Success 169 | Language preferences /account/language_preferences/ Success 170 | Current time zone /account/current_time_zone/ Success -------------------------------------------------------------------------------- /http-archive/aria-label/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aria-label", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "aria-label", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "aria-query": "^5.3.2", 13 | "cheerio": "^1.0.0" 14 | } 15 | }, 16 | "node_modules/aria-query": { 17 | "version": "5.3.2", 18 | "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", 19 | "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", 20 | "engines": { 21 | "node": ">= 0.4" 22 | } 23 | }, 24 | "node_modules/boolbase": { 25 | "version": "1.0.0", 26 | "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", 27 | "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" 28 | }, 29 | "node_modules/cheerio": { 30 | "version": "1.0.0", 31 | "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", 32 | "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", 33 | "dependencies": { 34 | "cheerio-select": "^2.1.0", 35 | "dom-serializer": "^2.0.0", 36 | "domhandler": "^5.0.3", 37 | "domutils": "^3.1.0", 38 | "encoding-sniffer": "^0.2.0", 39 | "htmlparser2": "^9.1.0", 40 | "parse5": "^7.1.2", 41 | "parse5-htmlparser2-tree-adapter": "^7.0.0", 42 | "parse5-parser-stream": "^7.1.2", 43 | "undici": "^6.19.5", 44 | "whatwg-mimetype": "^4.0.0" 45 | }, 46 | "engines": { 47 | "node": ">=18.17" 48 | }, 49 | "funding": { 50 | "url": "https://github.com/cheeriojs/cheerio?sponsor=1" 51 | } 52 | }, 53 | "node_modules/cheerio-select": { 54 | "version": "2.1.0", 55 | "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", 56 | "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", 57 | "dependencies": { 58 | "boolbase": "^1.0.0", 59 | "css-select": "^5.1.0", 60 | "css-what": "^6.1.0", 61 | "domelementtype": "^2.3.0", 62 | "domhandler": "^5.0.3", 63 | "domutils": "^3.0.1" 64 | }, 65 | "funding": { 66 | "url": "https://github.com/sponsors/fb55" 67 | } 68 | }, 69 | "node_modules/css-select": { 70 | "version": "5.1.0", 71 | "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", 72 | "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", 73 | "dependencies": { 74 | "boolbase": "^1.0.0", 75 | "css-what": "^6.1.0", 76 | "domhandler": "^5.0.2", 77 | "domutils": "^3.0.1", 78 | "nth-check": "^2.0.1" 79 | }, 80 | "funding": { 81 | "url": "https://github.com/sponsors/fb55" 82 | } 83 | }, 84 | "node_modules/css-what": { 85 | "version": "6.1.0", 86 | "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", 87 | "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", 88 | "engines": { 89 | "node": ">= 6" 90 | }, 91 | "funding": { 92 | "url": "https://github.com/sponsors/fb55" 93 | } 94 | }, 95 | "node_modules/dom-serializer": { 96 | "version": "2.0.0", 97 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 98 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 99 | "dependencies": { 100 | "domelementtype": "^2.3.0", 101 | "domhandler": "^5.0.2", 102 | "entities": "^4.2.0" 103 | }, 104 | "funding": { 105 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 106 | } 107 | }, 108 | "node_modules/domelementtype": { 109 | "version": "2.3.0", 110 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 111 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 112 | "funding": [ 113 | { 114 | "type": "github", 115 | "url": "https://github.com/sponsors/fb55" 116 | } 117 | ] 118 | }, 119 | "node_modules/domhandler": { 120 | "version": "5.0.3", 121 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 122 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 123 | "dependencies": { 124 | "domelementtype": "^2.3.0" 125 | }, 126 | "engines": { 127 | "node": ">= 4" 128 | }, 129 | "funding": { 130 | "url": "https://github.com/fb55/domhandler?sponsor=1" 131 | } 132 | }, 133 | "node_modules/domutils": { 134 | "version": "3.2.2", 135 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", 136 | "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", 137 | "dependencies": { 138 | "dom-serializer": "^2.0.0", 139 | "domelementtype": "^2.3.0", 140 | "domhandler": "^5.0.3" 141 | }, 142 | "funding": { 143 | "url": "https://github.com/fb55/domutils?sponsor=1" 144 | } 145 | }, 146 | "node_modules/encoding-sniffer": { 147 | "version": "0.2.0", 148 | "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", 149 | "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", 150 | "dependencies": { 151 | "iconv-lite": "^0.6.3", 152 | "whatwg-encoding": "^3.1.1" 153 | }, 154 | "funding": { 155 | "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" 156 | } 157 | }, 158 | "node_modules/entities": { 159 | "version": "4.5.0", 160 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 161 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 162 | "engines": { 163 | "node": ">=0.12" 164 | }, 165 | "funding": { 166 | "url": "https://github.com/fb55/entities?sponsor=1" 167 | } 168 | }, 169 | "node_modules/htmlparser2": { 170 | "version": "9.1.0", 171 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", 172 | "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", 173 | "funding": [ 174 | "https://github.com/fb55/htmlparser2?sponsor=1", 175 | { 176 | "type": "github", 177 | "url": "https://github.com/sponsors/fb55" 178 | } 179 | ], 180 | "dependencies": { 181 | "domelementtype": "^2.3.0", 182 | "domhandler": "^5.0.3", 183 | "domutils": "^3.1.0", 184 | "entities": "^4.5.0" 185 | } 186 | }, 187 | "node_modules/iconv-lite": { 188 | "version": "0.6.3", 189 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 190 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 191 | "dependencies": { 192 | "safer-buffer": ">= 2.1.2 < 3.0.0" 193 | }, 194 | "engines": { 195 | "node": ">=0.10.0" 196 | } 197 | }, 198 | "node_modules/nth-check": { 199 | "version": "2.1.1", 200 | "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", 201 | "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 202 | "dependencies": { 203 | "boolbase": "^1.0.0" 204 | }, 205 | "funding": { 206 | "url": "https://github.com/fb55/nth-check?sponsor=1" 207 | } 208 | }, 209 | "node_modules/parse5": { 210 | "version": "7.2.1", 211 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", 212 | "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", 213 | "dependencies": { 214 | "entities": "^4.5.0" 215 | }, 216 | "funding": { 217 | "url": "https://github.com/inikulin/parse5?sponsor=1" 218 | } 219 | }, 220 | "node_modules/parse5-htmlparser2-tree-adapter": { 221 | "version": "7.1.0", 222 | "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", 223 | "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", 224 | "dependencies": { 225 | "domhandler": "^5.0.3", 226 | "parse5": "^7.0.0" 227 | }, 228 | "funding": { 229 | "url": "https://github.com/inikulin/parse5?sponsor=1" 230 | } 231 | }, 232 | "node_modules/parse5-parser-stream": { 233 | "version": "7.1.2", 234 | "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", 235 | "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", 236 | "dependencies": { 237 | "parse5": "^7.0.0" 238 | }, 239 | "funding": { 240 | "url": "https://github.com/inikulin/parse5?sponsor=1" 241 | } 242 | }, 243 | "node_modules/safer-buffer": { 244 | "version": "2.1.2", 245 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 246 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 247 | }, 248 | "node_modules/undici": { 249 | "version": "6.21.2", 250 | "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", 251 | "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", 252 | "engines": { 253 | "node": ">=18.17" 254 | } 255 | }, 256 | "node_modules/whatwg-encoding": { 257 | "version": "3.1.1", 258 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", 259 | "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", 260 | "dependencies": { 261 | "iconv-lite": "0.6.3" 262 | }, 263 | "engines": { 264 | "node": ">=18" 265 | } 266 | }, 267 | "node_modules/whatwg-mimetype": { 268 | "version": "4.0.0", 269 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", 270 | "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", 271 | "engines": { 272 | "node": ">=18" 273 | } 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /accessibility/report-html.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | const issues = require("./data/pa11y.json"); 4 | const allScenarios = require("../ui/scenarios"); 5 | 6 | const scenarios = allScenarios 7 | // .reduce((list, scenario) => { 8 | // const states = scenario.states || []; 9 | // const newEntries = [scenario].concat(states); 10 | // return list.concat(newEntries); 11 | // }, []) 12 | .filter((s) => !Boolean(s.skip)); 13 | 14 | const categories = [...new Set(scenarios.map((s) => s.category))]; 15 | 16 | const scenariosByCategory = categories.reduce( 17 | (by, c) => ({ 18 | ...by, 19 | [c]: scenarios.filter((s) => s.category === c), 20 | }), 21 | {}, 22 | ); 23 | 24 | const issuesByScenario = issues.reduce((by, issue) => { 25 | by[issue.label] = by[issue.label] || []; 26 | 27 | by[issue.label].push(issue); 28 | 29 | return by; 30 | }, {}); 31 | 32 | /** 33 | * See https://stackoverflow.com/a/1054862/1798491. 34 | */ 35 | const slug = (str) => 36 | str 37 | .toLowerCase() 38 | .replace(/ /g, "-") 39 | .replace(/[-–]+/g, "-") 40 | .replace(/[^\w-]+/g, ""); 41 | 42 | /** 43 | * String concatenation disguised as lit-html, for syntax highlighting purposes. 44 | */ 45 | const html = (strings, ...values) => { 46 | return strings.reduce((out, str, i) => { 47 | const val = values[i] ?? ""; 48 | return out + str + val; 49 | }, ""); 50 | }; 51 | 52 | const githubCorner = html` 53 | 104 | `; 105 | 106 | const Scenario = (scenario) => { 107 | const { category, label, path, states } = scenario; 108 | const fullLabel = `${category} – ${label}`; 109 | const issues = issuesByScenario[fullLabel] || []; 110 | const axeIssues = issues.filter((i) => i.runner === "axe"); 111 | const htmlcsIssues = issues.filter((i) => i.runner === "htmlcs"); 112 | 113 | if (scenario.skip || scenario.skipReport) { 114 | return ""; 115 | } 116 | 117 | return html` 118 |
119 |
120 |
121 |

122 | ${label} 123 | 128 |

129 |

130 | Path: 131 | ${path} 134 |

135 |

136 | Lighthouse: 137 | ${fullLabel}.html 140 |

141 |
142 | Scenario 143 |
${JSON.stringify(scenario, null, 2)}
144 |
145 |
146 | Axe issues: ${axeIssues.length} 147 |
    148 | ${axeIssues 149 | .map( 150 | (issue) => html` 151 |
  • 152 |

    153 | ${issue.runner} ${issue.code}: 154 | ${issue.message} 155 |

    156 |

    ${issue.selector}

    157 |
  • 158 | `, 159 | ) 160 | .join("")} 161 |
162 |
163 |
164 | HTML_CS: ${htmlcsIssues.length} 165 |
    166 | ${htmlcsIssues 167 | .map( 168 | (issue) => html` 169 |
  • 170 |

    171 | ${issue.runner} ${issue.code}: 172 | ${issue.message} 173 |

    174 |

    ${issue.selector}

    175 |
  • 176 | `, 177 | ) 178 | .join("")} 179 |
180 |
181 |
182 | 187 | Screenshot of ${fullLabel} 194 | 195 |
196 | ${states 197 | ? states 198 | .map((s) => 199 | Scenario({ 200 | category, 201 | path, 202 | ...s, 203 | label: `${label} - ${s.label}`, 204 | }), 205 | ) 206 | .join("") 207 | : ""} 208 |
209 | `; 210 | }; 211 | 212 | const Category = (category, i) => { 213 | return html` 214 |
215 |

216 | ${category} 217 | 218 |

219 | ${scenariosByCategory[category].map(Scenario).join("")} 220 |
221 | `; 222 | }; 223 | 224 | const OverviewRow = (scenarioLabel) => { 225 | const issues = issuesByScenario[scenarioLabel]; 226 | const axeIssues = issues.filter((i) => i.runner === "axe"); 227 | const htmlcsIssues = issues.filter((i) => i.runner === "htmlcs"); 228 | 229 | if (issues.length === 0) { 230 | return html` 231 | No issues! 232 | `; 233 | } 234 | 235 | const { label, pageUrl, screenshot, lighthouseReport } = issues[0]; 236 | 237 | return html` 238 | 239 | ${label} 240 | ${axeIssues.length} 241 | ${htmlcsIssues.length} 242 | 243 | jump, path, 245 | screenshot, 250 | lighthouse 256 | 257 | 258 | `; 259 | }; 260 | 261 | const report = html` 262 | 263 | 264 | 265 | Wagtail admin tests report | Pa11y + Lighthouse 266 | 270 | 285 | 286 | 287 |
288 |

Wagtail admin accessibility tests

289 |

290 | Automated accessibility tests from 291 | github.com/thibaudcolas/wagtail-tooling. 294 |

295 |

296 | Generated: 297 | 298 |

299 |

300 | All issues: 301 | pa11y.json (large file) 302 |

303 |

304 | Running with Axe and HTML CodeSniffer via 305 | Pa11y, and Lighthouse. 306 |

307 |
308 |
309 |

310 | Overview 311 |

312 | 313 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | ${Object.keys(issuesByScenario).map(OverviewRow).join("")} 326 | 327 |
314 | Overview of test results 315 |
ScenarioAxe issuesHTML_CS issuesLinks
328 | ${categories.map(Category).join("")} 329 |
330 | 331 | 332 | 333 | `; 334 | 335 | fs.writeFileSync(`${__dirname}/data/report.html`, report); 336 | -------------------------------------------------------------------------------- /downloads-analysis/installer-stats.csv: -------------------------------------------------------------------------------- 1 | month,installer,num_downloads 2 | 2024-01-01 00:00:00.000000 UTC,pip,195135 3 | 2024-01-01 00:00:00.000000 UTC,requests,17320 4 | 2024-01-01 00:00:00.000000 UTC,poetry,13041 5 | 2024-01-01 00:00:00.000000 UTC,bandersnatch,3630 6 | 2024-01-01 00:00:00.000000 UTC,Nexus,2131 7 | 2024-01-01 00:00:00.000000 UTC,,1516 8 | 2024-01-01 00:00:00.000000 UTC,Browser,1159 9 | 2024-01-01 00:00:00.000000 UTC,pdm,580 10 | 2024-01-01 00:00:00.000000 UTC,OS,35 11 | 2024-01-01 00:00:00.000000 UTC,setuptools,31 12 | 2024-01-01 00:00:00.000000 UTC,devpi,11 13 | 2024-01-01 00:00:00.000000 UTC,conda,5 14 | 2024-02-01 00:00:00.000000 UTC,pip,192916 15 | 2024-02-01 00:00:00.000000 UTC,poetry,15444 16 | 2024-02-01 00:00:00.000000 UTC,requests,15261 17 | 2024-02-01 00:00:00.000000 UTC,,4930 18 | 2024-02-01 00:00:00.000000 UTC,bandersnatch,4598 19 | 2024-02-01 00:00:00.000000 UTC,Nexus,1648 20 | 2024-02-01 00:00:00.000000 UTC,pdm,595 21 | 2024-02-01 00:00:00.000000 UTC,Browser,566 22 | 2024-02-01 00:00:00.000000 UTC,devpi,109 23 | 2024-02-01 00:00:00.000000 UTC,OS,58 24 | 2024-02-01 00:00:00.000000 UTC,conda,8 25 | 2024-02-01 00:00:00.000000 UTC,setuptools,6 26 | 2024-02-01 00:00:00.000000 UTC,Artifactory,1 27 | 2024-03-01 00:00:00.000000 UTC,pip,188733 28 | 2024-03-01 00:00:00.000000 UTC,poetry,21879 29 | 2024-03-01 00:00:00.000000 UTC,,10946 30 | 2024-03-01 00:00:00.000000 UTC,requests,10431 31 | 2024-03-01 00:00:00.000000 UTC,bandersnatch,3663 32 | 2024-03-01 00:00:00.000000 UTC,Nexus,1410 33 | 2024-03-01 00:00:00.000000 UTC,Browser,717 34 | 2024-03-01 00:00:00.000000 UTC,pdm,528 35 | 2024-03-01 00:00:00.000000 UTC,OS,65 36 | 2024-03-01 00:00:00.000000 UTC,setuptools,32 37 | 2024-03-01 00:00:00.000000 UTC,devpi,8 38 | 2024-04-01 00:00:00.000000 UTC,pip,198626 39 | 2024-04-01 00:00:00.000000 UTC,poetry,22953 40 | 2024-04-01 00:00:00.000000 UTC,requests,11633 41 | 2024-04-01 00:00:00.000000 UTC,bandersnatch,5518 42 | 2024-04-01 00:00:00.000000 UTC,,3476 43 | 2024-04-01 00:00:00.000000 UTC,Browser,2758 44 | 2024-04-01 00:00:00.000000 UTC,Nexus,2104 45 | 2024-04-01 00:00:00.000000 UTC,uv,1089 46 | 2024-04-01 00:00:00.000000 UTC,pdm,264 47 | 2024-04-01 00:00:00.000000 UTC,OS,71 48 | 2024-04-01 00:00:00.000000 UTC,setuptools,17 49 | 2024-04-01 00:00:00.000000 UTC,devpi,16 50 | 2024-04-01 00:00:00.000000 UTC,conda,6 51 | 2024-05-01 00:00:00.000000 UTC,pip,212287 52 | 2024-05-01 00:00:00.000000 UTC,poetry,24414 53 | 2024-05-01 00:00:00.000000 UTC,requests,7388 54 | 2024-05-01 00:00:00.000000 UTC,uv,6727 55 | 2024-05-01 00:00:00.000000 UTC,bandersnatch,4511 56 | 2024-05-01 00:00:00.000000 UTC,Browser,3566 57 | 2024-05-01 00:00:00.000000 UTC,,1601 58 | 2024-05-01 00:00:00.000000 UTC,Nexus,1370 59 | 2024-05-01 00:00:00.000000 UTC,pdm,242 60 | 2024-05-01 00:00:00.000000 UTC,OS,39 61 | 2024-05-01 00:00:00.000000 UTC,conda,13 62 | 2024-05-01 00:00:00.000000 UTC,devpi,11 63 | 2024-05-01 00:00:00.000000 UTC,Artifactory,1 64 | 2024-05-01 00:00:00.000000 UTC,setuptools,1 65 | 2024-06-01 00:00:00.000000 UTC,pip,196752 66 | 2024-06-01 00:00:00.000000 UTC,poetry,24949 67 | 2024-06-01 00:00:00.000000 UTC,uv,15984 68 | 2024-06-01 00:00:00.000000 UTC,requests,6304 69 | 2024-06-01 00:00:00.000000 UTC,bandersnatch,3441 70 | 2024-06-01 00:00:00.000000 UTC,Nexus,1712 71 | 2024-06-01 00:00:00.000000 UTC,,1566 72 | 2024-06-01 00:00:00.000000 UTC,Browser,1274 73 | 2024-06-01 00:00:00.000000 UTC,pdm,368 74 | 2024-06-01 00:00:00.000000 UTC,OS,42 75 | 2024-06-01 00:00:00.000000 UTC,devpi,6 76 | 2024-07-01 00:00:00.000000 UTC,uv,565531 77 | 2024-07-01 00:00:00.000000 UTC,pip,230611 78 | 2024-07-01 00:00:00.000000 UTC,poetry,29661 79 | 2024-07-01 00:00:00.000000 UTC,requests,7214 80 | 2024-07-01 00:00:00.000000 UTC,bandersnatch,4020 81 | 2024-07-01 00:00:00.000000 UTC,Browser,2670 82 | 2024-07-01 00:00:00.000000 UTC,,1635 83 | 2024-07-01 00:00:00.000000 UTC,Nexus,1588 84 | 2024-07-01 00:00:00.000000 UTC,pdm,542 85 | 2024-07-01 00:00:00.000000 UTC,OS,62 86 | 2024-07-01 00:00:00.000000 UTC,devpi,18 87 | 2024-07-01 00:00:00.000000 UTC,conda,3 88 | 2024-07-01 00:00:00.000000 UTC,Artifactory,1 89 | 2024-08-01 00:00:00.000000 UTC,uv,493784 90 | 2024-08-01 00:00:00.000000 UTC,pip,202128 91 | 2024-08-01 00:00:00.000000 UTC,poetry,28644 92 | 2024-08-01 00:00:00.000000 UTC,requests,5773 93 | 2024-08-01 00:00:00.000000 UTC,bandersnatch,2825 94 | 2024-08-01 00:00:00.000000 UTC,,2155 95 | 2024-08-01 00:00:00.000000 UTC,Nexus,2044 96 | 2024-08-01 00:00:00.000000 UTC,Browser,819 97 | 2024-08-01 00:00:00.000000 UTC,pdm,363 98 | 2024-08-01 00:00:00.000000 UTC,OS,63 99 | 2024-08-01 00:00:00.000000 UTC,devpi,17 100 | 2024-08-01 00:00:00.000000 UTC,conda,6 101 | 2024-09-01 00:00:00.000000 UTC,pip,198922 102 | 2024-09-01 00:00:00.000000 UTC,uv,35069 103 | 2024-09-01 00:00:00.000000 UTC,poetry,29554 104 | 2024-09-01 00:00:00.000000 UTC,requests,6385 105 | 2024-09-01 00:00:00.000000 UTC,bandersnatch,4722 106 | 2024-09-01 00:00:00.000000 UTC,,2298 107 | 2024-09-01 00:00:00.000000 UTC,Nexus,1472 108 | 2024-09-01 00:00:00.000000 UTC,Browser,789 109 | 2024-09-01 00:00:00.000000 UTC,pdm,265 110 | 2024-09-01 00:00:00.000000 UTC,OS,58 111 | 2024-09-01 00:00:00.000000 UTC,devpi,5 112 | 2024-09-01 00:00:00.000000 UTC,conda,2 113 | 2024-09-01 00:00:00.000000 UTC,setuptools,2 114 | 2024-10-01 00:00:00.000000 UTC,pip,221391 115 | 2024-10-01 00:00:00.000000 UTC,poetry,32657 116 | 2024-10-01 00:00:00.000000 UTC,uv,30632 117 | 2024-10-01 00:00:00.000000 UTC,requests,17625 118 | 2024-10-01 00:00:00.000000 UTC,bandersnatch,3391 119 | 2024-10-01 00:00:00.000000 UTC,,2360 120 | 2024-10-01 00:00:00.000000 UTC,Browser,1714 121 | 2024-10-01 00:00:00.000000 UTC,Nexus,1063 122 | 2024-10-01 00:00:00.000000 UTC,pdm,192 123 | 2024-10-01 00:00:00.000000 UTC,OS,66 124 | 2024-10-01 00:00:00.000000 UTC,devpi,7 125 | 2024-10-01 00:00:00.000000 UTC,Artifactory,1 126 | 2024-11-01 00:00:00.000000 UTC,pip,201220 127 | 2024-11-01 00:00:00.000000 UTC,poetry,30466 128 | 2024-11-01 00:00:00.000000 UTC,uv,27054 129 | 2024-11-01 00:00:00.000000 UTC,requests,10247 130 | 2024-11-01 00:00:00.000000 UTC,bandersnatch,6264 131 | 2024-11-01 00:00:00.000000 UTC,,4239 132 | 2024-11-01 00:00:00.000000 UTC,Browser,2206 133 | 2024-11-01 00:00:00.000000 UTC,Nexus,1171 134 | 2024-11-01 00:00:00.000000 UTC,devpi,195 135 | 2024-11-01 00:00:00.000000 UTC,pdm,138 136 | 2024-11-01 00:00:00.000000 UTC,OS,72 137 | 2024-11-01 00:00:00.000000 UTC,conda,11 138 | 2024-12-01 00:00:00.000000 UTC,pip,166525 139 | 2024-12-01 00:00:00.000000 UTC,poetry,24038 140 | 2024-12-01 00:00:00.000000 UTC,uv,21861 141 | 2024-12-01 00:00:00.000000 UTC,requests,7440 142 | 2024-12-01 00:00:00.000000 UTC,bandersnatch,4968 143 | 2024-12-01 00:00:00.000000 UTC,,1165 144 | 2024-12-01 00:00:00.000000 UTC,Nexus,961 145 | 2024-12-01 00:00:00.000000 UTC,Browser,507 146 | 2024-12-01 00:00:00.000000 UTC,pdm,68 147 | 2024-12-01 00:00:00.000000 UTC,OS,19 148 | 2024-12-01 00:00:00.000000 UTC,devpi,7 149 | 2024-12-01 00:00:00.000000 UTC,conda,3 150 | 2025-01-01 00:00:00.000000 UTC,pip,189451 151 | 2025-01-01 00:00:00.000000 UTC,uv,37142 152 | 2025-01-01 00:00:00.000000 UTC,poetry,31884 153 | 2025-01-01 00:00:00.000000 UTC,requests,13798 154 | 2025-01-01 00:00:00.000000 UTC,bandersnatch,2742 155 | 2025-01-01 00:00:00.000000 UTC,,2384 156 | 2025-01-01 00:00:00.000000 UTC,Browser,1151 157 | 2025-01-01 00:00:00.000000 UTC,Nexus,1058 158 | 2025-01-01 00:00:00.000000 UTC,devpi,272 159 | 2025-01-01 00:00:00.000000 UTC,OS,55 160 | 2025-01-01 00:00:00.000000 UTC,pdm,19 161 | 2025-01-01 00:00:00.000000 UTC,conda,2 162 | 2025-02-01 00:00:00.000000 UTC,pip,201867 163 | 2025-02-01 00:00:00.000000 UTC,uv,55568 164 | 2025-02-01 00:00:00.000000 UTC,poetry,30467 165 | 2025-02-01 00:00:00.000000 UTC,requests,5952 166 | 2025-02-01 00:00:00.000000 UTC,bandersnatch,5949 167 | 2025-02-01 00:00:00.000000 UTC,,2691 168 | 2025-02-01 00:00:00.000000 UTC,Browser,816 169 | 2025-02-01 00:00:00.000000 UTC,Nexus,668 170 | 2025-02-01 00:00:00.000000 UTC,devpi,174 171 | 2025-02-01 00:00:00.000000 UTC,pdm,51 172 | 2025-02-01 00:00:00.000000 UTC,OS,40 173 | 2025-02-01 00:00:00.000000 UTC,conda,9 174 | 2025-03-01 00:00:00.000000 UTC,pip,228356 175 | 2025-03-01 00:00:00.000000 UTC,uv,53131 176 | 2025-03-01 00:00:00.000000 UTC,poetry,30363 177 | 2025-03-01 00:00:00.000000 UTC,requests,7320 178 | 2025-03-01 00:00:00.000000 UTC,bandersnatch,3996 179 | 2025-03-01 00:00:00.000000 UTC,,1542 180 | 2025-03-01 00:00:00.000000 UTC,Browser,1009 181 | 2025-03-01 00:00:00.000000 UTC,Nexus,611 182 | 2025-03-01 00:00:00.000000 UTC,pdm,42 183 | 2025-03-01 00:00:00.000000 UTC,OS,41 184 | 2025-03-01 00:00:00.000000 UTC,devpi,40 185 | 2025-03-01 00:00:00.000000 UTC,setuptools,1 186 | 2025-04-01 00:00:00.000000 UTC,pip,198022 187 | 2025-04-01 00:00:00.000000 UTC,uv,70496 188 | 2025-04-01 00:00:00.000000 UTC,poetry,31490 189 | 2025-04-01 00:00:00.000000 UTC,requests,8016 190 | 2025-04-01 00:00:00.000000 UTC,bandersnatch,5624 191 | 2025-04-01 00:00:00.000000 UTC,,1724 192 | 2025-04-01 00:00:00.000000 UTC,Browser,1097 193 | 2025-04-01 00:00:00.000000 UTC,Nexus,435 194 | 2025-04-01 00:00:00.000000 UTC,pdm,112 195 | 2025-04-01 00:00:00.000000 UTC,OS,25 196 | 2025-04-01 00:00:00.000000 UTC,devpi,10 197 | 2025-04-01 00:00:00.000000 UTC,setuptools,1 198 | 2025-05-01 00:00:00.000000 UTC,pip,277606 199 | 2025-05-01 00:00:00.000000 UTC,uv,64820 200 | 2025-05-01 00:00:00.000000 UTC,poetry,29748 201 | 2025-05-01 00:00:00.000000 UTC,requests,6867 202 | 2025-05-01 00:00:00.000000 UTC,bandersnatch,3178 203 | 2025-05-01 00:00:00.000000 UTC,,2124 204 | 2025-05-01 00:00:00.000000 UTC,Browser,1053 205 | 2025-05-01 00:00:00.000000 UTC,Nexus,316 206 | 2025-05-01 00:00:00.000000 UTC,pdm,144 207 | 2025-05-01 00:00:00.000000 UTC,OS,15 208 | 2025-05-01 00:00:00.000000 UTC,devpi,12 209 | 2025-05-01 00:00:00.000000 UTC,conda,2 210 | 2025-05-01 00:00:00.000000 UTC,Artifactory,2 211 | 2025-05-01 00:00:00.000000 UTC,setuptools,2 212 | 2025-05-01 00:00:00.000000 UTC,Bazel,1 213 | 2025-06-01 00:00:00.000000 UTC,pip,487535 214 | 2025-06-01 00:00:00.000000 UTC,uv,70390 215 | 2025-06-01 00:00:00.000000 UTC,poetry,34523 216 | 2025-06-01 00:00:00.000000 UTC,requests,3745 217 | 2025-06-01 00:00:00.000000 UTC,,3671 218 | 2025-06-01 00:00:00.000000 UTC,bandersnatch,2953 219 | 2025-06-01 00:00:00.000000 UTC,Browser,1627 220 | 2025-06-01 00:00:00.000000 UTC,Nexus,284 221 | 2025-06-01 00:00:00.000000 UTC,pdm,83 222 | 2025-06-01 00:00:00.000000 UTC,setuptools,58 223 | 2025-06-01 00:00:00.000000 UTC,devpi,26 224 | 2025-06-01 00:00:00.000000 UTC,OS,23 225 | 2025-07-01 00:00:00.000000 UTC,pip,301127 226 | 2025-07-01 00:00:00.000000 UTC,uv,98735 227 | 2025-07-01 00:00:00.000000 UTC,poetry,32608 228 | 2025-07-01 00:00:00.000000 UTC,bandersnatch,4697 229 | 2025-07-01 00:00:00.000000 UTC,,3627 230 | 2025-07-01 00:00:00.000000 UTC,requests,3543 231 | 2025-07-01 00:00:00.000000 UTC,Browser,1328 232 | 2025-07-01 00:00:00.000000 UTC,Nexus,328 233 | 2025-07-01 00:00:00.000000 UTC,pdm,216 234 | 2025-07-01 00:00:00.000000 UTC,OS,77 235 | 2025-07-01 00:00:00.000000 UTC,devpi,10 236 | 2025-08-01 00:00:00.000000 UTC,pip,288135 237 | 2025-08-01 00:00:00.000000 UTC,uv,103384 238 | 2025-08-01 00:00:00.000000 UTC,,27555 239 | 2025-08-01 00:00:00.000000 UTC,poetry,27344 240 | 2025-08-01 00:00:00.000000 UTC,requests,7509 241 | 2025-08-01 00:00:00.000000 UTC,bandersnatch,7037 242 | 2025-08-01 00:00:00.000000 UTC,Browser,1133 243 | 2025-08-01 00:00:00.000000 UTC,Nexus,218 244 | 2025-08-01 00:00:00.000000 UTC,devpi,81 245 | 2025-08-01 00:00:00.000000 UTC,pdm,47 246 | 2025-08-01 00:00:00.000000 UTC,OS,32 247 | 2025-08-01 00:00:00.000000 UTC,setuptools,3 248 | 2025-09-01 00:00:00.000000 UTC,pip,272434 249 | 2025-09-01 00:00:00.000000 UTC,uv,139466 250 | 2025-09-01 00:00:00.000000 UTC,,42159 251 | 2025-09-01 00:00:00.000000 UTC,poetry,25624 252 | 2025-09-01 00:00:00.000000 UTC,bandersnatch,6604 253 | 2025-09-01 00:00:00.000000 UTC,requests,3462 254 | 2025-09-01 00:00:00.000000 UTC,Browser,711 255 | 2025-09-01 00:00:00.000000 UTC,Nexus,257 256 | 2025-09-01 00:00:00.000000 UTC,OS,68 257 | 2025-09-01 00:00:00.000000 UTC,devpi,12 258 | 2025-09-01 00:00:00.000000 UTC,pdm,6 259 | -------------------------------------------------------------------------------- /downloads-analysis/ci-installer-stats.csv: -------------------------------------------------------------------------------- 1 | month,installer,ci,num_downloads 2 | 2024-01-01 00:00:00.000000 UTC,pip,,195135 3 | 2024-01-01 00:00:00.000000 UTC,requests,,17320 4 | 2024-01-01 00:00:00.000000 UTC,poetry,,13041 5 | 2024-01-01 00:00:00.000000 UTC,bandersnatch,,3630 6 | 2024-01-01 00:00:00.000000 UTC,Nexus,,2131 7 | 2024-01-01 00:00:00.000000 UTC,,,1516 8 | 2024-01-01 00:00:00.000000 UTC,Browser,,1159 9 | 2024-01-01 00:00:00.000000 UTC,pdm,,580 10 | 2024-01-01 00:00:00.000000 UTC,OS,,35 11 | 2024-01-01 00:00:00.000000 UTC,setuptools,,31 12 | 2024-01-01 00:00:00.000000 UTC,devpi,,11 13 | 2024-01-01 00:00:00.000000 UTC,conda,,5 14 | 2024-02-01 00:00:00.000000 UTC,pip,,163305 15 | 2024-02-01 00:00:00.000000 UTC,pip,true,29611 16 | 2024-02-01 00:00:00.000000 UTC,poetry,,15444 17 | 2024-02-01 00:00:00.000000 UTC,requests,,15261 18 | 2024-02-01 00:00:00.000000 UTC,,,4930 19 | 2024-02-01 00:00:00.000000 UTC,bandersnatch,,4598 20 | 2024-02-01 00:00:00.000000 UTC,Nexus,,1648 21 | 2024-02-01 00:00:00.000000 UTC,pdm,,595 22 | 2024-02-01 00:00:00.000000 UTC,Browser,,566 23 | 2024-02-01 00:00:00.000000 UTC,devpi,,109 24 | 2024-02-01 00:00:00.000000 UTC,OS,,58 25 | 2024-02-01 00:00:00.000000 UTC,conda,,8 26 | 2024-02-01 00:00:00.000000 UTC,setuptools,,6 27 | 2024-02-01 00:00:00.000000 UTC,Artifactory,,1 28 | 2024-03-01 00:00:00.000000 UTC,pip,,131973 29 | 2024-03-01 00:00:00.000000 UTC,pip,true,56760 30 | 2024-03-01 00:00:00.000000 UTC,poetry,,21879 31 | 2024-03-01 00:00:00.000000 UTC,,,10946 32 | 2024-03-01 00:00:00.000000 UTC,requests,,10431 33 | 2024-03-01 00:00:00.000000 UTC,bandersnatch,,3663 34 | 2024-03-01 00:00:00.000000 UTC,Nexus,,1410 35 | 2024-03-01 00:00:00.000000 UTC,Browser,,717 36 | 2024-03-01 00:00:00.000000 UTC,pdm,,528 37 | 2024-03-01 00:00:00.000000 UTC,OS,,65 38 | 2024-03-01 00:00:00.000000 UTC,setuptools,,32 39 | 2024-03-01 00:00:00.000000 UTC,devpi,,8 40 | 2024-04-01 00:00:00.000000 UTC,pip,,133433 41 | 2024-04-01 00:00:00.000000 UTC,pip,true,65193 42 | 2024-04-01 00:00:00.000000 UTC,poetry,,22953 43 | 2024-04-01 00:00:00.000000 UTC,requests,,11633 44 | 2024-04-01 00:00:00.000000 UTC,bandersnatch,,5518 45 | 2024-04-01 00:00:00.000000 UTC,,,3476 46 | 2024-04-01 00:00:00.000000 UTC,Browser,,2758 47 | 2024-04-01 00:00:00.000000 UTC,Nexus,,2104 48 | 2024-04-01 00:00:00.000000 UTC,uv,true,642 49 | 2024-04-01 00:00:00.000000 UTC,uv,,447 50 | 2024-04-01 00:00:00.000000 UTC,pdm,,264 51 | 2024-04-01 00:00:00.000000 UTC,OS,,71 52 | 2024-04-01 00:00:00.000000 UTC,setuptools,,17 53 | 2024-04-01 00:00:00.000000 UTC,devpi,,16 54 | 2024-04-01 00:00:00.000000 UTC,conda,,6 55 | 2024-05-01 00:00:00.000000 UTC,pip,,141406 56 | 2024-05-01 00:00:00.000000 UTC,pip,true,70881 57 | 2024-05-01 00:00:00.000000 UTC,poetry,,24414 58 | 2024-05-01 00:00:00.000000 UTC,requests,,7388 59 | 2024-05-01 00:00:00.000000 UTC,bandersnatch,,4511 60 | 2024-05-01 00:00:00.000000 UTC,uv,,4377 61 | 2024-05-01 00:00:00.000000 UTC,Browser,,3566 62 | 2024-05-01 00:00:00.000000 UTC,uv,true,2350 63 | 2024-05-01 00:00:00.000000 UTC,,,1601 64 | 2024-05-01 00:00:00.000000 UTC,Nexus,,1370 65 | 2024-05-01 00:00:00.000000 UTC,pdm,,242 66 | 2024-05-01 00:00:00.000000 UTC,OS,,39 67 | 2024-05-01 00:00:00.000000 UTC,conda,,13 68 | 2024-05-01 00:00:00.000000 UTC,devpi,,11 69 | 2024-05-01 00:00:00.000000 UTC,Artifactory,,1 70 | 2024-05-01 00:00:00.000000 UTC,setuptools,,1 71 | 2024-06-01 00:00:00.000000 UTC,pip,,132355 72 | 2024-06-01 00:00:00.000000 UTC,pip,true,64397 73 | 2024-06-01 00:00:00.000000 UTC,poetry,,24949 74 | 2024-06-01 00:00:00.000000 UTC,uv,,12376 75 | 2024-06-01 00:00:00.000000 UTC,requests,,6304 76 | 2024-06-01 00:00:00.000000 UTC,uv,true,3608 77 | 2024-06-01 00:00:00.000000 UTC,bandersnatch,,3441 78 | 2024-06-01 00:00:00.000000 UTC,Nexus,,1712 79 | 2024-06-01 00:00:00.000000 UTC,,,1566 80 | 2024-06-01 00:00:00.000000 UTC,Browser,,1274 81 | 2024-06-01 00:00:00.000000 UTC,pdm,,368 82 | 2024-06-01 00:00:00.000000 UTC,OS,,42 83 | 2024-06-01 00:00:00.000000 UTC,devpi,,6 84 | 2024-07-01 00:00:00.000000 UTC,uv,true,550367 85 | 2024-07-01 00:00:00.000000 UTC,pip,,168848 86 | 2024-07-01 00:00:00.000000 UTC,pip,true,61763 87 | 2024-07-01 00:00:00.000000 UTC,poetry,,29661 88 | 2024-07-01 00:00:00.000000 UTC,uv,,15164 89 | 2024-07-01 00:00:00.000000 UTC,requests,,7214 90 | 2024-07-01 00:00:00.000000 UTC,bandersnatch,,4020 91 | 2024-07-01 00:00:00.000000 UTC,Browser,,2670 92 | 2024-07-01 00:00:00.000000 UTC,,,1635 93 | 2024-07-01 00:00:00.000000 UTC,Nexus,,1588 94 | 2024-07-01 00:00:00.000000 UTC,pdm,,542 95 | 2024-07-01 00:00:00.000000 UTC,OS,,62 96 | 2024-07-01 00:00:00.000000 UTC,devpi,,18 97 | 2024-07-01 00:00:00.000000 UTC,conda,,3 98 | 2024-07-01 00:00:00.000000 UTC,Artifactory,,1 99 | 2024-08-01 00:00:00.000000 UTC,uv,true,480549 100 | 2024-08-01 00:00:00.000000 UTC,pip,,145180 101 | 2024-08-01 00:00:00.000000 UTC,pip,true,56948 102 | 2024-08-01 00:00:00.000000 UTC,poetry,,28644 103 | 2024-08-01 00:00:00.000000 UTC,uv,,13235 104 | 2024-08-01 00:00:00.000000 UTC,requests,,5773 105 | 2024-08-01 00:00:00.000000 UTC,bandersnatch,,2825 106 | 2024-08-01 00:00:00.000000 UTC,,,2155 107 | 2024-08-01 00:00:00.000000 UTC,Nexus,,2044 108 | 2024-08-01 00:00:00.000000 UTC,Browser,,819 109 | 2024-08-01 00:00:00.000000 UTC,pdm,,363 110 | 2024-08-01 00:00:00.000000 UTC,OS,,63 111 | 2024-08-01 00:00:00.000000 UTC,devpi,,17 112 | 2024-08-01 00:00:00.000000 UTC,conda,,6 113 | 2024-09-01 00:00:00.000000 UTC,pip,,139590 114 | 2024-09-01 00:00:00.000000 UTC,pip,true,59332 115 | 2024-09-01 00:00:00.000000 UTC,poetry,,29554 116 | 2024-09-01 00:00:00.000000 UTC,uv,,19135 117 | 2024-09-01 00:00:00.000000 UTC,uv,true,15934 118 | 2024-09-01 00:00:00.000000 UTC,requests,,6385 119 | 2024-09-01 00:00:00.000000 UTC,bandersnatch,,4722 120 | 2024-09-01 00:00:00.000000 UTC,,,2298 121 | 2024-09-01 00:00:00.000000 UTC,Nexus,,1472 122 | 2024-09-01 00:00:00.000000 UTC,Browser,,789 123 | 2024-09-01 00:00:00.000000 UTC,pdm,,265 124 | 2024-09-01 00:00:00.000000 UTC,OS,,58 125 | 2024-09-01 00:00:00.000000 UTC,devpi,,5 126 | 2024-09-01 00:00:00.000000 UTC,conda,,2 127 | 2024-09-01 00:00:00.000000 UTC,setuptools,,2 128 | 2024-10-01 00:00:00.000000 UTC,pip,,161068 129 | 2024-10-01 00:00:00.000000 UTC,pip,true,60323 130 | 2024-10-01 00:00:00.000000 UTC,poetry,,32657 131 | 2024-10-01 00:00:00.000000 UTC,uv,true,18455 132 | 2024-10-01 00:00:00.000000 UTC,requests,,17625 133 | 2024-10-01 00:00:00.000000 UTC,uv,,12177 134 | 2024-10-01 00:00:00.000000 UTC,bandersnatch,,3391 135 | 2024-10-01 00:00:00.000000 UTC,,,2360 136 | 2024-10-01 00:00:00.000000 UTC,Browser,,1714 137 | 2024-10-01 00:00:00.000000 UTC,Nexus,,1063 138 | 2024-10-01 00:00:00.000000 UTC,pdm,,192 139 | 2024-10-01 00:00:00.000000 UTC,OS,,66 140 | 2024-10-01 00:00:00.000000 UTC,devpi,,7 141 | 2024-10-01 00:00:00.000000 UTC,Artifactory,,1 142 | 2024-11-01 00:00:00.000000 UTC,pip,,138475 143 | 2024-11-01 00:00:00.000000 UTC,pip,true,62745 144 | 2024-11-01 00:00:00.000000 UTC,poetry,,30466 145 | 2024-11-01 00:00:00.000000 UTC,uv,true,15346 146 | 2024-11-01 00:00:00.000000 UTC,uv,,11708 147 | 2024-11-01 00:00:00.000000 UTC,requests,,10247 148 | 2024-11-01 00:00:00.000000 UTC,bandersnatch,,6264 149 | 2024-11-01 00:00:00.000000 UTC,,,4239 150 | 2024-11-01 00:00:00.000000 UTC,Browser,,2206 151 | 2024-11-01 00:00:00.000000 UTC,Nexus,,1171 152 | 2024-11-01 00:00:00.000000 UTC,devpi,,195 153 | 2024-11-01 00:00:00.000000 UTC,pdm,,138 154 | 2024-11-01 00:00:00.000000 UTC,OS,,72 155 | 2024-11-01 00:00:00.000000 UTC,conda,,11 156 | 2024-12-01 00:00:00.000000 UTC,pip,,121501 157 | 2024-12-01 00:00:00.000000 UTC,pip,true,45024 158 | 2024-12-01 00:00:00.000000 UTC,poetry,,24038 159 | 2024-12-01 00:00:00.000000 UTC,uv,true,12280 160 | 2024-12-01 00:00:00.000000 UTC,uv,,9581 161 | 2024-12-01 00:00:00.000000 UTC,requests,,7440 162 | 2024-12-01 00:00:00.000000 UTC,bandersnatch,,4968 163 | 2024-12-01 00:00:00.000000 UTC,,,1165 164 | 2024-12-01 00:00:00.000000 UTC,Nexus,,961 165 | 2024-12-01 00:00:00.000000 UTC,Browser,,507 166 | 2024-12-01 00:00:00.000000 UTC,pdm,,68 167 | 2024-12-01 00:00:00.000000 UTC,OS,,19 168 | 2024-12-01 00:00:00.000000 UTC,devpi,,7 169 | 2024-12-01 00:00:00.000000 UTC,conda,,3 170 | 2025-01-01 00:00:00.000000 UTC,pip,,135939 171 | 2025-01-01 00:00:00.000000 UTC,pip,true,53512 172 | 2025-01-01 00:00:00.000000 UTC,poetry,,31884 173 | 2025-01-01 00:00:00.000000 UTC,uv,true,21135 174 | 2025-01-01 00:00:00.000000 UTC,uv,,16007 175 | 2025-01-01 00:00:00.000000 UTC,requests,,13798 176 | 2025-01-01 00:00:00.000000 UTC,bandersnatch,,2742 177 | 2025-01-01 00:00:00.000000 UTC,,,2384 178 | 2025-01-01 00:00:00.000000 UTC,Browser,,1151 179 | 2025-01-01 00:00:00.000000 UTC,Nexus,,1058 180 | 2025-01-01 00:00:00.000000 UTC,devpi,,272 181 | 2025-01-01 00:00:00.000000 UTC,OS,,55 182 | 2025-01-01 00:00:00.000000 UTC,pdm,,19 183 | 2025-01-01 00:00:00.000000 UTC,conda,,2 184 | 2025-02-01 00:00:00.000000 UTC,pip,,154371 185 | 2025-02-01 00:00:00.000000 UTC,pip,true,47496 186 | 2025-02-01 00:00:00.000000 UTC,uv,true,31761 187 | 2025-02-01 00:00:00.000000 UTC,poetry,,30467 188 | 2025-02-01 00:00:00.000000 UTC,uv,,23807 189 | 2025-02-01 00:00:00.000000 UTC,requests,,5952 190 | 2025-02-01 00:00:00.000000 UTC,bandersnatch,,5949 191 | 2025-02-01 00:00:00.000000 UTC,,,2691 192 | 2025-02-01 00:00:00.000000 UTC,Browser,,816 193 | 2025-02-01 00:00:00.000000 UTC,Nexus,,668 194 | 2025-02-01 00:00:00.000000 UTC,devpi,,174 195 | 2025-02-01 00:00:00.000000 UTC,pdm,,51 196 | 2025-02-01 00:00:00.000000 UTC,OS,,40 197 | 2025-02-01 00:00:00.000000 UTC,conda,,9 198 | 2025-03-01 00:00:00.000000 UTC,pip,,181872 199 | 2025-03-01 00:00:00.000000 UTC,pip,true,46484 200 | 2025-03-01 00:00:00.000000 UTC,uv,true,33403 201 | 2025-03-01 00:00:00.000000 UTC,poetry,,30363 202 | 2025-03-01 00:00:00.000000 UTC,uv,,19728 203 | 2025-03-01 00:00:00.000000 UTC,requests,,7320 204 | 2025-03-01 00:00:00.000000 UTC,bandersnatch,,3996 205 | 2025-03-01 00:00:00.000000 UTC,,,1542 206 | 2025-03-01 00:00:00.000000 UTC,Browser,,1009 207 | 2025-03-01 00:00:00.000000 UTC,Nexus,,611 208 | 2025-03-01 00:00:00.000000 UTC,pdm,,42 209 | 2025-03-01 00:00:00.000000 UTC,OS,,41 210 | 2025-03-01 00:00:00.000000 UTC,devpi,,40 211 | 2025-03-01 00:00:00.000000 UTC,setuptools,,1 212 | 2025-04-01 00:00:00.000000 UTC,pip,,149549 213 | 2025-04-01 00:00:00.000000 UTC,pip,true,48473 214 | 2025-04-01 00:00:00.000000 UTC,uv,true,45396 215 | 2025-04-01 00:00:00.000000 UTC,poetry,,31490 216 | 2025-04-01 00:00:00.000000 UTC,uv,,25100 217 | 2025-04-01 00:00:00.000000 UTC,requests,,8016 218 | 2025-04-01 00:00:00.000000 UTC,bandersnatch,,5624 219 | 2025-04-01 00:00:00.000000 UTC,,,1724 220 | 2025-04-01 00:00:00.000000 UTC,Browser,,1097 221 | 2025-04-01 00:00:00.000000 UTC,Nexus,,435 222 | 2025-04-01 00:00:00.000000 UTC,pdm,,112 223 | 2025-04-01 00:00:00.000000 UTC,OS,,25 224 | 2025-04-01 00:00:00.000000 UTC,devpi,,10 225 | 2025-04-01 00:00:00.000000 UTC,setuptools,,1 226 | 2025-05-01 00:00:00.000000 UTC,pip,,220545 227 | 2025-05-01 00:00:00.000000 UTC,pip,true,57061 228 | 2025-05-01 00:00:00.000000 UTC,uv,true,40274 229 | 2025-05-01 00:00:00.000000 UTC,poetry,,29748 230 | 2025-05-01 00:00:00.000000 UTC,uv,,24546 231 | 2025-05-01 00:00:00.000000 UTC,requests,,6867 232 | 2025-05-01 00:00:00.000000 UTC,bandersnatch,,3178 233 | 2025-05-01 00:00:00.000000 UTC,,,2124 234 | 2025-05-01 00:00:00.000000 UTC,Browser,,1053 235 | 2025-05-01 00:00:00.000000 UTC,Nexus,,316 236 | 2025-05-01 00:00:00.000000 UTC,pdm,,144 237 | 2025-05-01 00:00:00.000000 UTC,OS,,15 238 | 2025-05-01 00:00:00.000000 UTC,devpi,,12 239 | 2025-05-01 00:00:00.000000 UTC,conda,,2 240 | 2025-05-01 00:00:00.000000 UTC,setuptools,,2 241 | 2025-05-01 00:00:00.000000 UTC,Artifactory,,2 242 | 2025-05-01 00:00:00.000000 UTC,Bazel,,1 243 | 2025-06-01 00:00:00.000000 UTC,pip,,389267 244 | 2025-06-01 00:00:00.000000 UTC,pip,true,98268 245 | 2025-06-01 00:00:00.000000 UTC,uv,true,42883 246 | 2025-06-01 00:00:00.000000 UTC,poetry,,34523 247 | 2025-06-01 00:00:00.000000 UTC,uv,,27507 248 | 2025-06-01 00:00:00.000000 UTC,requests,,3745 249 | 2025-06-01 00:00:00.000000 UTC,,,3671 250 | 2025-06-01 00:00:00.000000 UTC,bandersnatch,,2953 251 | 2025-06-01 00:00:00.000000 UTC,Browser,,1627 252 | 2025-06-01 00:00:00.000000 UTC,Nexus,,284 253 | 2025-06-01 00:00:00.000000 UTC,pdm,,83 254 | 2025-06-01 00:00:00.000000 UTC,setuptools,,58 255 | 2025-06-01 00:00:00.000000 UTC,devpi,,26 256 | 2025-06-01 00:00:00.000000 UTC,OS,,23 257 | 2025-07-01 00:00:00.000000 UTC,pip,,222566 258 | 2025-07-01 00:00:00.000000 UTC,pip,true,78561 259 | 2025-07-01 00:00:00.000000 UTC,uv,true,61871 260 | 2025-07-01 00:00:00.000000 UTC,uv,,36864 261 | 2025-07-01 00:00:00.000000 UTC,poetry,,32608 262 | 2025-07-01 00:00:00.000000 UTC,bandersnatch,,4697 263 | 2025-07-01 00:00:00.000000 UTC,,,3627 264 | 2025-07-01 00:00:00.000000 UTC,requests,,3543 265 | 2025-07-01 00:00:00.000000 UTC,Browser,,1328 266 | 2025-07-01 00:00:00.000000 UTC,Nexus,,328 267 | 2025-07-01 00:00:00.000000 UTC,pdm,,216 268 | 2025-07-01 00:00:00.000000 UTC,OS,,77 269 | 2025-07-01 00:00:00.000000 UTC,devpi,,10 270 | 2025-08-01 00:00:00.000000 UTC,pip,,228067 271 | 2025-08-01 00:00:00.000000 UTC,uv,true,67426 272 | 2025-08-01 00:00:00.000000 UTC,pip,true,60068 273 | 2025-08-01 00:00:00.000000 UTC,uv,,35958 274 | 2025-08-01 00:00:00.000000 UTC,,,27555 275 | 2025-08-01 00:00:00.000000 UTC,poetry,,27344 276 | 2025-08-01 00:00:00.000000 UTC,requests,,7509 277 | 2025-08-01 00:00:00.000000 UTC,bandersnatch,,7037 278 | 2025-08-01 00:00:00.000000 UTC,Browser,,1133 279 | 2025-08-01 00:00:00.000000 UTC,Nexus,,218 280 | 2025-08-01 00:00:00.000000 UTC,devpi,,81 281 | 2025-08-01 00:00:00.000000 UTC,pdm,,47 282 | 2025-08-01 00:00:00.000000 UTC,OS,,32 283 | 2025-08-01 00:00:00.000000 UTC,setuptools,,3 284 | 2025-09-01 00:00:00.000000 UTC,pip,,228701 285 | 2025-09-01 00:00:00.000000 UTC,uv,true,84599 286 | 2025-09-01 00:00:00.000000 UTC,uv,,54867 287 | 2025-09-01 00:00:00.000000 UTC,pip,true,43733 288 | 2025-09-01 00:00:00.000000 UTC,,,42159 289 | 2025-09-01 00:00:00.000000 UTC,poetry,,25624 290 | 2025-09-01 00:00:00.000000 UTC,bandersnatch,,6604 291 | 2025-09-01 00:00:00.000000 UTC,requests,,3462 292 | 2025-09-01 00:00:00.000000 UTC,Browser,,711 293 | 2025-09-01 00:00:00.000000 UTC,Nexus,,257 294 | 2025-09-01 00:00:00.000000 UTC,OS,,68 295 | 2025-09-01 00:00:00.000000 UTC,devpi,,12 296 | 2025-09-01 00:00:00.000000 UTC,pdm,,6 297 | 2025-10-01 00:00:00.000000 UTC,pip,,29574 298 | 2025-10-01 00:00:00.000000 UTC,uv,true,10253 299 | 2025-10-01 00:00:00.000000 UTC,uv,,7900 300 | 2025-10-01 00:00:00.000000 UTC,pip,true,6231 301 | 2025-10-01 00:00:00.000000 UTC,,,6153 302 | 2025-10-01 00:00:00.000000 UTC,poetry,,3958 303 | 2025-10-01 00:00:00.000000 UTC,requests,,300 304 | 2025-10-01 00:00:00.000000 UTC,Browser,,294 305 | 2025-10-01 00:00:00.000000 UTC,Nexus,,37 306 | 2025-10-01 00:00:00.000000 UTC,bandersnatch,,14 307 | 2025-10-01 00:00:00.000000 UTC,OS,,9 308 | 2025-10-01 00:00:00.000000 UTC,devpi,,4 309 | --------------------------------------------------------------------------------