├── .gitignore
├── LICENSE
├── README.md
├── admin
├── pageaudit
│ ├── README.md
│ ├── _dev
│ │ ├── ssl-cert-snakeoil.crt
│ │ ├── ssl-cert-snakeoil.key
│ │ └── ssl-cert-snakeoil.pem
│ ├── createfixture.sh
│ ├── createmodeldoc.sh
│ ├── documentation
│ │ ├── PageLab pres.key
│ │ ├── PageLab pres.pdf
│ │ ├── Roadmap.key
│ │ └── pagelab_models.png
│ ├── manage.py
│ ├── pageaudit
│ │ ├── __init__.py
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ ├── report
│ │ ├── __init__.py
│ │ ├── admin.py
│ │ ├── apps.py
│ │ ├── context_processors.py
│ │ ├── helpers.py
│ │ ├── import_csv.py
│ │ ├── migrations
│ │ │ ├── 0001_initial.py
│ │ │ ├── 0002_auto_20181026_1224.py
│ │ │ ├── 0003_auto_20181026_1558.py
│ │ │ ├── 0004_url_user_timings_migrated.py
│ │ │ ├── 0005_remove_url_user_timings_migrated.py
│ │ │ ├── 0006_auto_20181107_1651.py
│ │ │ ├── 0007_auto_20181107_1652.py
│ │ │ ├── 0008_auto_20181108_0952.py
│ │ │ ├── 0009_auto_20181108_1041.py
│ │ │ ├── 0010_auto_20181108_1413.py
│ │ │ ├── 0011_auto_20181108_1417.py
│ │ │ ├── 0012_auto_20181109_1225.py
│ │ │ ├── 0012_auto_20181113_1510.py
│ │ │ ├── 0013_auto_20181129_1047.py
│ │ │ ├── 0014_usertimingmeasurename_team.py
│ │ │ ├── 0015_auto_20181130_1123.py
│ │ │ └── __init__.py
│ │ ├── models.py
│ │ ├── templates
│ │ │ ├── 404.html
│ │ │ ├── 500.html
│ │ │ ├── home.html
│ │ │ ├── page_template.html
│ │ │ ├── page_template_flatpages.html
│ │ │ ├── partials
│ │ │ │ ├── audit_score_donut.html
│ │ │ │ ├── auth_form.html
│ │ │ │ ├── banner_notification.html
│ │ │ │ ├── compare_item.html
│ │ │ │ ├── compare_tray.html
│ │ │ │ ├── home_load_items.html
│ │ │ │ ├── modal.html
│ │ │ │ ├── modal_redirects.html
│ │ │ │ ├── report_card.html
│ │ │ │ ├── report_detail_table_run_kpi_rows.html
│ │ │ │ ├── url_filter_select.html
│ │ │ │ ├── urls_compare_2_table.html
│ │ │ │ └── urls_compare_3_table.html
│ │ │ ├── reports_browse.html
│ │ │ ├── reports_dashboard.html
│ │ │ ├── reports_filters.html
│ │ │ ├── reports_lighthouse_viewer.html
│ │ │ ├── reports_lighthouse_viewer_template.html
│ │ │ ├── reports_urls_compare.html
│ │ │ ├── reports_urls_detail_noruns.html
│ │ │ ├── reports_urls_detail_withruns.html
│ │ │ ├── signedout.html
│ │ │ └── signin.html
│ │ ├── templatetags
│ │ │ ├── bannernotification.py
│ │ │ ├── beautify.py
│ │ │ ├── define.py
│ │ │ ├── flatpages_nav_highlight.py
│ │ │ ├── pageview.py
│ │ │ ├── scoredisplayoptions.py
│ │ │ └── template_helpers.py
│ │ ├── tests
│ │ │ ├── __init__.py
│ │ │ └── test_filters.py
│ │ ├── urls.py
│ │ └── views.py
│ ├── runserver
│ └── static
│ │ └── report
│ │ ├── css
│ │ ├── c3.min.css
│ │ ├── datatables
│ │ │ ├── colReorder.dataTables.min.css
│ │ │ ├── images
│ │ │ │ ├── sort_asc.png
│ │ │ │ ├── sort_both.png
│ │ │ │ └── sort_desc.png
│ │ │ ├── jquery.dataTables.min.css
│ │ │ └── responsive.dataTables.min.css
│ │ ├── hint.base.min.css
│ │ ├── images
│ │ │ ├── sort_asc.png
│ │ │ ├── sort_both.png
│ │ │ └── sort_desc.png
│ │ ├── modal.css
│ │ ├── select2.min.css
│ │ ├── site-custom.css
│ │ └── tachyons.min.css
│ │ ├── img
│ │ ├── chevron-forward.svg
│ │ ├── django.svg
│ │ ├── leadspace.svg
│ │ ├── nodejs.svg
│ │ ├── notests.png
│ │ ├── postgresql.png
│ │ └── redis.svg
│ │ ├── js
│ │ ├── c3.min.js
│ │ ├── compare.js
│ │ ├── d3.v4.min.js
│ │ ├── jquery-3.3.1.min.js
│ │ ├── jquery.datatables.pkg.min.js
│ │ ├── micromodal.min.js
│ │ ├── select2.min.js
│ │ ├── site-base.js
│ │ ├── url-typeahead.js
│ │ └── util-storage.js
│ │ └── lighthouse-viewer
│ │ ├── images
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ └── lh_logo_bg.png
│ │ ├── manifest.json
│ │ ├── src
│ │ ├── polyfills
│ │ │ ├── fetch.js
│ │ │ └── url-search-params.js
│ │ └── viewer.js
│ │ ├── styles
│ │ └── viewer.css
│ │ └── sw.js
└── requirements.txt
└── pageaudit
├── README.md
├── audit-localstorage.js
├── config.js
├── gather-local-storage.js
├── gather-localstorage.js
├── package-lock.json
├── package.json
└── pagelab.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pot
3 | *.py[co]
4 | .tox/
5 | .vscode/
6 | __pycache__
7 | MANIFEST
8 | .DS_Store
9 | build/
10 | db.sqlite3
11 | dist/
12 | docs/_build/
13 | docs/locale/
14 | dump.rdb
15 | node_modules/
16 | tests/.coverage
17 | tests/coverage_html/
18 | tests/report/
19 | _local
20 | settings_local.py
21 | admin/pageaudit/static/CACHE/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Page Lab
2 |
3 | Web Page Performance Laboratory: Scaling [Lighthouse](https://github.com/GoogleChrome/Lighthouse "Google Lighthouse") performance and web testing tools
4 |
5 | * This is Alpha software and needs some testing and automation. PR's are accepted!
6 |
7 | ## Page Lab is an attempt at understanding web performance at scale
8 |
9 | The goals here are 3-fold:
10 |
11 | ## Understand page performance now (and provide a historical record)
12 |
13 | * Easy automated Lighthouse tests of any URL
14 | * A history of each test run, for as many runs as desired
15 | * Historical data will allow us to track performance of our pages over time, giving us insight into when changes cause performance regressions
16 | * Coupled with the Web Timing API, Page Lab will be able to drill into any included scripts (properly instrumented) and understand what EXACTLY is impacting page performance
17 |
18 | ## Get started
19 |
20 | ### Setup Django Lighthouse reporting app
21 |
22 | * see: [Django README](admin/pageaudit/README.md "Django README")
23 |
24 | ### Setup Node testing server
25 |
26 | * see: [node README](pageaudit/README.md "Node README")
27 |
28 | ## Roadmap & Ideas
29 |
30 | ### Automate _some_ fixes for pages
31 |
32 | * For instance, being able to tell developers that certain scripts are not even used on the page
33 | * Automation of image compression to the proper smaller size
34 | * Automatically give guidance on which assets can be preloaded, etc
35 |
36 | ## Pre-flight check tool for newly published pages
37 |
38 | * Developers can run their page through *Pag Lab* and see what can be fixed - some of which will be automated for them, e.g.: compressed web assets, etc
39 |
--------------------------------------------------------------------------------
/admin/pageaudit/README.md:
--------------------------------------------------------------------------------
1 | # PageLab - Reporting app
2 |
3 | * The PageLab reporting app is built on Django. It consumes Lighthouse report data objects and visualizes URL audits, averages and historical page trends.
4 |
5 | ## Installation
6 |
7 | **Dependencies**
8 |
9 | * node 8+
10 | * python 3+
11 | * postgres 9.6+
12 | * [PageLab node app](../../pageaudit)
13 |
14 |
15 | **Note**: We are using python 3. If you already have python 2 setup and mapped to `python` command, you will need to use `python3`. Or you can setup a virtual env. and make life easy for yourself and have `python` mapped to `python3`.
16 |
17 |
18 |
19 | ## Getting started - First time install:
20 |
21 | 1. Ensure you have the dependencies installed.
22 | 2. Clone the repo.
23 | 3. From the repo root directory, run `pip3 install -r admin/requirements.txt` to install Django and all it's requirements for the app.
24 | 4. There are some local variables and settings needed for your implementation. They can either be set as environment variables, or you can add a `settings_local.py` file alongside the Django default `settings.py` file in the `admin/pageaudit/pageaudit/` directory with them.
25 | Replace the `___` with your local Postgres DB user ID/PW.
26 |
27 |
28 | ```
29 | DJANGO_DB_HOST=127.0.0.1
30 | DJANGO_DB_PASSWORD=____
31 | DJANGO_DB_USER=____
32 | DJANGO_DEBUG_FLAG=True
33 | DJANGO_ENV=production
34 | DJANGO_FORCE_SCRIPT_NAME=
35 | ```
36 | - Create a database called `perf_lab` (default), or create a variable called `DJANGO_DB_NAME` and set it to your local database name.
37 |
38 |
39 |
40 | ## Getting started - Sample data:
41 | A sample data set is available to be loaded via Django's `manage.py loaddata` command. The sample data set contains:
42 | - 51 URLs (50 with runs)
43 | - Each URL has ~13 test runs.
44 | - Superuser with ID/PW: `superuser` / `django4ever`
45 | - The sample data set file is available here: https://github.com/ecumike/page-lab-sampledata
46 |
47 |
48 | ## Getting started - Coding:
49 | - Run `./manage.py migrate` so Django can create and setup your database as needed.
50 | - Start the app by running `runserver` (in this root directory).
51 | - To view the site, open a browser to `https://localhost:8000/report/`
52 | - To view the Django admin, goto `https://localhost:8000/admin/`
53 | - We try and follow the Django and Python coding design and style guides as found here:
54 | - https://docs.djangoproject.com/en/2.0/misc/design-philosophies/
55 | - https://docs.djangoproject.com/en/2.0/internals/contributing/writing-code/coding-style/
56 |
57 |
58 | ## Populating data
59 | You can pre-populate the app with the sample data set as above, and/or you can add URLs via the Django admin and run a few test runs via the Node app.
60 | - To populate the PageLab Django app with data, goto the Django admin and add a couple URLs to test.
61 | - Install the [PageLab node app](../../pageaudit).
62 | - Run the PageLab node app.
63 | - The PageLab node app will test each URL you have in the Django app once, then stop.
64 | - Go back and view the site at `https://localhost:8000/report/` and you should see some reports.
65 |
66 |
67 | ## Design
68 | We are using:
69 | - [Tachyons](https://tachyons.io/) for the main app theme.
70 | - [Eva icons](https://akveo.github.io/eva-icons/#/) for the icons.
71 | - [Hint.css](https://kushagragour.in/lab/hint/) for the tooltip.
72 | - [Micromodal.js](https://micromodal.now.sh/) for the modal overlays.
73 |
74 |
--------------------------------------------------------------------------------
/admin/pageaudit/_dev/ssl-cert-snakeoil.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIC7jCCAdYCCAHj/HJCEkgAMA0GCSqGSIb3DQEBCwUAMDQxHDAaBgNVBAMME1Vu
3 | dHJ1c3RlZCBBdXRob3JpdHkxFDASBgNVBAoMC1NlbGYtU2lnbmVkMB4XDTE4MDcy
4 | MzE0NTc1OFoXDTE5MDcyMzE0NTc1OFowPzEhMB8GA1UEAwwYKi5sb2NhbGhvc3Qv
5 | Q049bG9jYWxob3N0MRowGAYDVQQKDBFEdW1teSBDZXJ0aWZpY2F0ZTCCASIwDQYJ
6 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBAP4mCXLsI9C0ZrlGz6nQQSEv6TgMcPKK
7 | /HkJvXPHl0OZRqf1Juw1ztpfbdjY86EhWMkORbt2yf0QZrUc0FhrhtD3lbr5s8nY
8 | Fmc4qAKIMWkSbcQowl4OToMpwHi9CFO+u3LZffGAFhvbCOe0diIsMMR/VV6n2/Bd
9 | Fmu88uWR4R536IwWpI/Pa7mSQgNii4a1B/naaBFAUAG9qTreeS4Cz3z66SCpL40z
10 | tas5scVuuj0FYDTs90agQlxMF9DSrgHc2pv2IxpLBLzX9J6uGEvfEdRsBM9TEx5I
11 | qeByhrxcZFKWdbtD7mr8IftyP5dCnaIhWMXwNYbS1akQ/8PMhKyflHsCAwEAATAN
12 | BgkqhkiG9w0BAQsFAAOCAQEAKfFcMkZ3ia/H6Qs5zNK3KNKzq/dXIvw8DG4qt8XR
13 | D03iMarf6G56P0FcurTEbxy6BlWyY9xzHs5/os0hoPt+ZhfH/PMN9j2b1IHqwDoC
14 | gPyGUtm8KaM/XtaSayIzfpfdUKU4iWKMiBg3YL+0NCKD93GrxHuIBRVkKI4ieSvw
15 | 67zJcGr6/Gn6ZPEgnuJY7Z2Ca3AnXG2E1stAJ06CPHt6/Zd3ymtE/UWnxhguYSt5
16 | q+KLThTulmoKViGj7EZ1EQ4Vz4R3TkPcTBFLv9mvZBAkkuyFZGSAys0fzWfJQrWp
17 | 7UCpTPXMgOZ57Rsfx3+qQSS1iAw9MH1k+xM1/xG234xiJQ==
18 | -----END CERTIFICATE-----
19 |
--------------------------------------------------------------------------------
/admin/pageaudit/_dev/ssl-cert-snakeoil.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD+Jgly7CPQtGa5
3 | Rs+p0EEhL+k4DHDyivx5Cb1zx5dDmUan9SbsNc7aX23Y2POhIVjJDkW7dsn9EGa1
4 | HNBYa4bQ95W6+bPJ2BZnOKgCiDFpEm3EKMJeDk6DKcB4vQhTvrty2X3xgBYb2wjn
5 | tHYiLDDEf1Vep9vwXRZrvPLlkeEed+iMFqSPz2u5kkIDYouGtQf52mgRQFABvak6
6 | 3nkuAs98+ukgqS+NM7WrObHFbro9BWA07PdGoEJcTBfQ0q4B3Nqb9iMaSwS81/Se
7 | rhhL3xHUbATPUxMeSKngcoa8XGRSlnW7Q+5q/CH7cj+XQp2iIVjF8DWG0tWpEP/D
8 | zISsn5R7AgMBAAECggEBALtBDU82e8EHWnSCnvkQbxxjr6NTT1j1XMZVjdgjo+jz
9 | oyKXN0Fs9+bfzxCp8P+0kTU6JdjU0kh58CLbgwrS1vdapGCqIHTEmOUe0nNjI3y2
10 | TlfsbCFeyyxK8/xB00PY0UXyx5/Lbv7BKNOHnE7bXmc5Jo5igWOdQqrJAfJxxHts
11 | B/QhFvgdHrHO0fv1ULC2MO9Ud0vnr+/0blY9fNa7U5cRoiWdYXuxnJOxJWagxId8
12 | ic++brW0KVv4BSzIA8scM9qBIBjFmJc6r8fzq7Ll89dUlQBfvzi5hatv21QoQujO
13 | DaFYXzrq6682PY7tIj5rCBXWg0o3j8CxJw0rH47EPEECgYEA/xvzLd7P56X1qRbQ
14 | DgcAee6hGdPok0WqEh29OQNbTCZpDBEfY/cVvEqI8lDTrqyM/z1PGdiPy8Nk3BYJ
15 | 2SIHxo2DcxbVW9FO3Bgp1nzZYV8dYCFPmH1l/K9gGy00ocNVPh8o/Ek249yNgfNC
16 | 6CQYl6dtWaTwByfbm/5zyqVD91sCgYEA/wk6cL3sSxAvIrhVaXa9qN4KjBTKZYI/
17 | 6RAZcjvtGxsfEkb1cGnuycR1Qfugc7atwmTVlokoBf1o3OEPsKyTf1C6qMsRWIAg
18 | 1XTWrZk2ZZOSbZ9ceBYjQ6WgJmTdyTPCZQARSza3aFIr263Feu7PINSM2hC6CC4K
19 | 1bpRebnOgWECgYEAya7rHkSc0WKfSMLEUZKvibZinuytXmEhB5mDU2OX9igXvHZ8
20 | /qcFBAtZIVlNQTchcVijBKf1Zv5e6rBxsLv6sbqHRaGzpBdh5RclXHDv2s87hhhP
21 | uRrKWm676EBg79Jhve8ck/e98X8YULhlGOoQlzTCerCvIrkcIcOU/4yQoykCgYAN
22 | 7nceyYEq6Itqnh8sT6w0mUyCMnCL8v2CwbpiHxvoqyabXPzzUxYUN4MgQ5qUN5pu
23 | UAvK2VsyWJFt3213/TVhcwt/RPiBmR4yCtvfR8tM6S7KhjYK6Uqr21RQRJpI72bj
24 | FYncTfTe4f47Vda/zGPMK1A2aUuAPuOgoTjqKVg6gQKBgQDF+1wx5+Un33BEObDu
25 | BTeSd4qwHIp6Umqq2UsXZZycqKLYLF+QgeEYT9cRzF9lBeKU3Nd/mTjdtrjjQCUM
26 | DIFCufuEMXQIfbctRZIoKx2wXHl79YpMfe6dQ5UXxGSPR5fUgwyaRa6xe+0lZDBI
27 | 13y1+WUQVhP0rTx3fj5ZhxdI2g==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/admin/pageaudit/_dev/ssl-cert-snakeoil.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIICfDCCAWQCCQCp3TcHiSb15TANBgkqhkiG9w0BAQUFADAAMB4XDTE4MDcyMzE0
3 | NDgxNloXDTIxMDQxODE0NDgxNlowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
4 | AQoCggEBANtxRoQ7LNKlD7mKmVVpYqQCKEIosQgSl2ZJmQEbVKD0ojNPVT/Zj9JZ
5 | oq6gPC7Dw/7QkhI8JIfFEpUDCNim3n2jjYTc/ExaAGTKC4IfqBw5hjJCsNPU8uFr
6 | NfX9tJ0lXzK+rw0AES+dWRdTZSO2Leo7QrnsIMiD0prjJF6/eOJjVZkvxI8swQ4f
7 | 6xwQMSkXQogST5BF6tBLhZut2BkKIhEVUu/RR3l/tg+CXkALMuqX8QnW0yE6R6V7
8 | u4UdfKMimJi6MB7SHifI5ceMrPAnSvqekvn2718hXeOjZDQoEsXP/PKb75Cub5tN
9 | fGP4qe4Amp/tPQzEzsMqCo8Kk6Z/QY0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
10 | eAroKZGKp15XTdNKvt+2WD6xM2h5xUPRyZdF8rfFCXW1r+PRXrWCmRQoD4hKkz2t
11 | 1BgNphheJ3eoQTR+58jNypHaOPNG7tWvXGkgX96acS3J9r0h1ELwg+LjZ2AIKo13
12 | 9vT+rjPnohRkcmN2cvjM38okIwpUZj7w3gBNKV0H0GI/ntItMHL3hAIsB1zuntL9
13 | xHoMTV7LCYrHpY4hxDItaEp3MmQKpAxGfQWnDS31FJ+wTs0R0h/NxBoC+EFC4V2n
14 | U2XCMKxMXazXVWbjoeL3/1C+wE4qQCTjrkTMZQu0EyvpaTmZjZffo0ubhdiVMZ1Z
15 | CiwTL2uTBQO1pFyV/llkpQ==
16 | -----END CERTIFICATE-----
17 |
--------------------------------------------------------------------------------
/admin/pageaudit/createfixture.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | python3 manage.py dumpdata auth.User auth.Group report --indent 4 > report/fixtures/fixture-for-dev.json
--------------------------------------------------------------------------------
/admin/pageaudit/createmodeldoc.sh:
--------------------------------------------------------------------------------
1 | python manage.py graph_models -a -g -o documentation/pagelab_models.png
--------------------------------------------------------------------------------
/admin/pageaudit/documentation/PageLab pres.key:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/documentation/PageLab pres.key
--------------------------------------------------------------------------------
/admin/pageaudit/documentation/PageLab pres.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/documentation/PageLab pres.pdf
--------------------------------------------------------------------------------
/admin/pageaudit/documentation/Roadmap.key:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/documentation/Roadmap.key
--------------------------------------------------------------------------------
/admin/pageaudit/documentation/pagelab_models.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/documentation/pagelab_models.png
--------------------------------------------------------------------------------
/admin/pageaudit/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pageaudit.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 | execute_from_command_line(sys.argv)
16 |
--------------------------------------------------------------------------------
/admin/pageaudit/pageaudit/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/pageaudit/__init__.py
--------------------------------------------------------------------------------
/admin/pageaudit/pageaudit/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for pagelabproject.
3 |
4 | Generated by 'django-admin startproject' using Django 2.0.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.0/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 |
17 | ## BASE_DIR is where your manage.py is.
18 | ## SETTINGS_PATH is BASE_DIR + your_project_name (where settings.py is).
19 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
20 | SETTINGS_PATH = os.path.normpath(os.path.dirname(__file__))
21 |
22 |
23 | DATA_UPLOAD_MAX_MEMORY_SIZE = 36214400
24 | # Quick-start development settings - unsuitable for production
25 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
26 |
27 | # SECURITY WARNING: keep the secret key used in production secret!
28 | SECRET_KEY = 'qhk(v0g!4#(+_$$36hyks$nx!wkq$g&8qfgb92)92e)jkm1g%a'
29 |
30 | # SECURITY WARNING: don't run with debug turned on in production!
31 | # To test Django in "debug = false" mode, but using Django to server static files as in dev,
32 | # run this locally: `manage.py runserver --insecure`
33 | DEBUG = os.getenv('DJANGO_DEBUG_FLAG', False)
34 |
35 | ALLOWED_HOSTS = ['127.0.0.1', 'localhost']
36 |
37 | if os.getenv('DJANGO_ALLOWED_HOST'):
38 | ALLOWED_HOSTS.append(os.getenv('DJANGO_ALLOWED_HOST'))
39 |
40 | INTERNAL_IPS = ['127.0.0.1',]
41 |
42 | DBBACKUP_STORAGE = 'django.core.files.storage.FileSystemStorage'
43 | DBBACKUP_STORAGE_OPTIONS = {
44 | 'location': os.getenv('DJANGO_PAGELAB_DBBACKUP_PATH', '')
45 | }
46 | DBBACKUP_DATE_FORMAT = 'date-%d-%H'
47 | DBBACKUP_FILENAME_TEMPLATE = 'pagelab-{datetime}.{extension}'
48 |
49 |
50 | FORCE_SCRIPT_NAME = os.getenv('DJANGO_FORCE_SCRIPT_NAME', '/pagelab')
51 |
52 | # Application definition
53 |
54 | INSTALLED_APPS = [
55 | 'django.contrib.admin',
56 | 'django.contrib.auth',
57 | 'django.contrib.contenttypes',
58 | 'django.contrib.sessions',
59 | 'django.contrib.messages',
60 | 'django.contrib.staticfiles',
61 | 'django.contrib.flatpages',
62 | 'django.contrib.sites',
63 | 'django_extensions',
64 | 'report',
65 | 'dbbackup',
66 | 'compressor',
67 | ]
68 |
69 | MIDDLEWARE = [
70 | 'django.middleware.security.SecurityMiddleware',
71 | 'django.contrib.sessions.middleware.SessionMiddleware',
72 | 'django.middleware.common.CommonMiddleware',
73 | 'django.middleware.csrf.CsrfViewMiddleware',
74 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
75 | 'django.contrib.messages.middleware.MessageMiddleware',
76 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
77 | ]
78 |
79 | if DEBUG:
80 | MIDDLEWARE.append(
81 | 'debug_toolbar.middleware.DebugToolbarMiddleware')
82 | INSTALLED_APPS.append('debug_toolbar')
83 |
84 | ROOT_URLCONF = 'pageaudit.urls'
85 |
86 | TEMPLATES = [
87 | {
88 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
89 | 'DIRS': [
90 | os.path.join(SETTINGS_PATH, '../templates'),
91 | 'templates',
92 | 'report/templates',
93 | ],
94 | 'APP_DIRS': True,
95 | 'OPTIONS': {
96 | 'context_processors': [
97 | 'django.template.context_processors.debug',
98 | 'django.template.context_processors.request',
99 | 'django.contrib.auth.context_processors.auth',
100 | 'django.contrib.messages.context_processors.messages',
101 | 'report.context_processors.global_settings',
102 | ],
103 | },
104 | },
105 | ]
106 |
107 | WSGI_APPLICATION = 'pageaudit.wsgi.application'
108 |
109 |
110 | ## Admins get 500 errors
111 | ## Managers get 404 errors
112 | ADMINS = []
113 | MANAGERS = []
114 | ADMINS_EMAIL_TO_SMS = []
115 |
116 |
117 | # Database
118 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
119 | DATABASES = {
120 | 'default': {
121 | 'ENGINE': 'django.db.backends.postgresql',
122 | 'NAME': os.getenv('DJANGO_DB_NAME', 'perf_lab'),
123 | 'USER': os.getenv('DJANGO_DB_USER', 'perf_lab'),
124 | 'PASSWORD': os.getenv('DJANGO_DB_PASSWORD', ''),
125 | 'HOST': os.getenv('DJANGO_DB_HOST', 'localhost'),
126 | 'PORT': os.getenv('DJANGO_DB_PORT', 5432),
127 | }
128 | }
129 |
130 | # Password validation
131 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
132 |
133 | AUTH_PASSWORD_VALIDATORS = [
134 | {
135 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
136 | },
137 | {
138 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
139 | },
140 | {
141 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
142 | },
143 | {
144 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
145 | },
146 | ]
147 |
148 |
149 | # Internationalization
150 | # https://docs.djangoproject.com/en/2.0/topics/i18n/
151 |
152 | LANGUAGE_CODE = 'en-us'
153 |
154 | TIME_ZONE = 'America/New_York'
155 |
156 | USE_I18N = True
157 |
158 | USE_L10N = True
159 |
160 | USE_TZ = True
161 |
162 | SITE_ID = 1
163 |
164 | # Static files (CSS, JavaScript, Images)
165 | # https://docs.djangoproject.com/en/2.0/howto/static-files/
166 |
167 | STATICFILES_FINDERS = (
168 | ## Django defaults:
169 | 'django.contrib.staticfiles.finders.FileSystemFinder',
170 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
171 | ## Other modules:
172 | 'compressor.finders.CompressorFinder',
173 | )
174 |
175 | STATICFILES_DIRS = [
176 | os.path.join(BASE_DIR, "static/"),
177 | ]
178 |
179 | ## The directory on your filesystem where you want static files copied and served by YOUR WEB SERVER.
180 | ## When you run 'collectstatic', it copies them here.
181 | ## This is a production-only setting. It's not used in DEBUG mode.
182 | ## In DEBUG mode, files are served BY DJANGO from STATICFILES_DIRS var directory above.
183 | STATIC_ROOT = os.getenv('DJANGO_STATIC_ROOT', '')
184 |
185 | ## The URL path from your app home, where static files will be served from (their URL base path)
186 | ## In DEBUG mode, files serve from your STATICFILES_DIRS directory.
187 | ## In production/non-DEBUG mode, files serve from your STATIC_ROOT filesystem path dir.
188 | STATIC_URL = os.getenv('DJANGO_STATIC_URL', '/static-pagelab/')
189 | MEDIA_URL = os.getenv('DJANGO_MEDIA_URL', '/media/')
190 |
191 |
192 | ## Compressor module settings.
193 | ## Compressor is default set to OPPOSITE of DEBUG.
194 | ## To force compressor locally during debug, add "COMPRESS_ENABLED = True" var to your settings_local.py
195 | COMPRESS_ROOT = os.path.join(BASE_DIR, "static/")
196 | COMPRESS_CSS_FILTERS = [
197 | 'compressor.filters.css_default.CssAbsoluteFilter',
198 | 'compressor.filters.cssmin.rCSSMinFilter'
199 | ]
200 |
201 | ## Custom signin, signout, and post-signout page.
202 | LOGIN_URL = '%s/report/signin/' % FORCE_SCRIPT_NAME
203 | LOGOUT_URL = '%s/report/signout/' % FORCE_SCRIPT_NAME
204 | LOGOUT_REDIRECT_URL = '%s/report/signedout/' % FORCE_SCRIPT_NAME
205 |
206 |
207 | AUTHENTICATION_BACKENDS = [
208 | 'django.contrib.auth.backends.ModelBackend',
209 | ]
210 |
211 |
212 | ## Local settings override for ease, instead of/in addition to using ENV vars.
213 | ## Create a settings_local.py and override any vars above, even if they were set in your ENV.
214 | try:
215 | from .settings_local import *
216 | except ImportError:
217 | pass
218 |
219 |
--------------------------------------------------------------------------------
/admin/pageaudit/pageaudit/urls.py:
--------------------------------------------------------------------------------
1 | """pageaudit URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.conf.urls import url, include
17 | from django.contrib import admin
18 | from django.views.generic import TemplateView
19 | from django.conf import settings
20 | from django.conf.urls import url, include
21 | from django.conf.urls.static import static
22 | from django.contrib.auth.views import logout
23 | from django.views.generic import RedirectView
24 | from django.urls import reverse_lazy
25 |
26 | handler404 = 'report.views.custom_404'
27 | handler500 = 'report.views.custom_500'
28 |
29 | from report.views import collect_report, get_urls
30 |
31 | urlpatterns = [
32 | ## Django overall admin.
33 | url('admin/', admin.site.urls),
34 |
35 | ## Node URLs for running reports and posting them to us.
36 | url(r'^collect/report/$', collect_report, name='collect_report'),
37 | url(r'^queue/$', get_urls, name='get_urls'),
38 |
39 | ## Report app URLs namespace. All URLs are in reports/urls.py
40 | url(r'^report/', include(('report.urls', 'plr'))),
41 |
42 | ## Redirect /pagelab/ to real app home page.
43 | url(r'^(/)?$', RedirectView.as_view(url=reverse_lazy('plr:home'))),
44 |
45 | ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
46 |
47 |
48 | if settings.DEBUG:
49 | import debug_toolbar
50 | urlpatterns = [
51 | url(r'^__debug__/', include(debug_toolbar.urls)),
52 | ] + urlpatterns
53 |
54 |
--------------------------------------------------------------------------------
/admin/pageaudit/pageaudit/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for pageaudit project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
8 | """
9 | import os, sys
10 | import django
11 | from django.core.handlers.wsgi import WSGIHandler
12 |
13 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../")))
14 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../")))
15 |
16 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pageaudit.settings")
17 |
18 | env_variables_to_pass = ['DJANGO_ENV',
19 | 'DJANGO_DEBUG_FLAG',
20 | 'DJANGO_DB_NAME',
21 | 'DJANGO_DB_USER',
22 | 'DJANGO_DB_PASSWORD',
23 | 'DJANGO_DB_HOST',
24 | 'DJANGO_STATIC_ROOT',
25 | 'DJANGO_STATIC_URL',
26 | 'DJANGO_FORCE_SCRIPT_NAME',
27 | # 'DJANGO_LOGIN_URL',
28 | 'DJANGO_LOGOUT_URL',]
29 |
30 | class WSGIEnvironment(WSGIHandler):
31 | def __call__(self, environ, start_response):
32 | for var in env_variables_to_pass:
33 | os.environ[var] = environ.get(var, '')
34 | return super(WSGIEnvironment, self).__call__(environ, start_response)
35 |
36 | django.setup(set_prefix=False)
37 | application = WSGIEnvironment()
38 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/report/__init__.py
--------------------------------------------------------------------------------
/admin/pageaudit/report/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | from .models import *
4 |
5 | class LighthouseRunAdmin(admin.ModelAdmin):
6 | readonly_fields = ["url"]
7 |
8 | class UrlAdmin(admin.ModelAdmin):
9 | search_fields = ["url"]
10 | readonly_fields = ["lighthouse_run", "url_kpi_average", "url_paths", "search_key_vals"]
11 |
12 | class UrlKpiAverageAdmin(admin.ModelAdmin):
13 | readonly_fields = ["url"]
14 |
15 | class LighthouseDataRawAdmin(admin.ModelAdmin):
16 | readonly_fields = ["lighthouse_run"]
17 |
18 | class UserTimingMeasureAdmin(admin.ModelAdmin):
19 | readonly_fields = ["name", "url", "lighthouse_run"]
20 |
21 | class UserTimingMeasureAverageAdmin(admin.ModelAdmin):
22 | readonly_fields = ["name", "url"]
23 |
24 |
25 | admin.site.register(BannerNotification)
26 | admin.site.register(LighthouseDataRaw, LighthouseDataRawAdmin)
27 | admin.site.register(LighthouseDataUsertiming)
28 | admin.site.register(LighthouseRun, LighthouseRunAdmin)
29 | admin.site.register(PageView)
30 | admin.site.register(Team)
31 | admin.site.register(Url, UrlAdmin)
32 | admin.site.register(UrlKpiAverage, UrlKpiAverageAdmin)
33 | admin.site.register(UserTimingMeasure, UserTimingMeasureAdmin)
34 | admin.site.register(UserTimingMeasureAverage, UserTimingMeasureAverageAdmin)
35 | admin.site.register(UserTimingMeasureName)
36 | admin.site.register(UrlFilter)
37 | admin.site.register(UrlFilterPart)
38 | admin.site.register(UrlOwner)
39 | admin.site.register(SearchKeyVal)
40 | admin.site.register(UrlPath)
41 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ReportConfig(AppConfig):
5 | name = 'report'
6 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 | # Return any necessary/allowed values from settings.py here, to be used in templates.
4 | # Usage is just like any other var using double braces. These just set global var.
5 | def global_settings(request):
6 | return {
7 | 'FORCE_SCRIPT_NAME': settings.FORCE_SCRIPT_NAME
8 | }
9 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/helpers.py:
--------------------------------------------------------------------------------
1 | import os
2 | import datetime
3 | import requests, json
4 |
5 | from django.contrib.auth.models import User
6 | from django.core.mail import send_mail
7 |
8 |
9 | ## Global var to be used any time we need to use the range or min/max # of
10 | ## Google's category scoring scale. Names changed for more global usage.
11 | ## Ex: An accessibility score isn't "slow", it's "poor".
12 | ## Comes from Google documentation: https://developers.google.com/web/tools/lighthouse/v3/scoring
13 | ## Categories #s updated in 3.1.1
14 | ##
15 | ##
16 | GOOGLE_SCORE_SCALE = {
17 | "poor": {
18 | "min": 0,
19 | "max": 49
20 | },
21 | "average": {
22 | "min": 50,
23 | "max": 89
24 | },
25 | "good": {
26 | "min": 90,
27 | "max": 100
28 | }
29 | }
30 |
31 |
32 | ##
33 | ## User timings is an array of generic non-property-named objects.
34 | ## To get the one you want, you have to loop thru the array and find the one with the name you want.
35 | ## This is a common function that will do that for you.
36 | ## Simply pass the name of the timing you want, and the ARRAY OF OBJECTS to loop thru.
37 | ## @return {object} The requred timing object which you can then get the values (duration, start, etc) from.
38 | ##
39 | ##
40 | def getUserTimingValue(timingName, userTimingsObject=None, userTimingsArray=None):
41 | ## Allows us to pass top level Lighthouse report "user-timing" object
42 | ## in here and single try/catch.
43 | ## Otherwise, assume an array of objects
44 | returnObject = {
45 | "name": "",
46 | "duration": 0,
47 | "startTime": 0,
48 | "timingType": ""
49 | }
50 |
51 | try:
52 | userTimings = userTimingsObject['details']['items']
53 | except Exception as ex:
54 | userTimings = userTimingsArray
55 |
56 | ## Try and find the user-timing they requested, else return an empty one.
57 | try:
58 | for item in userTimings:
59 | if item['name'] == timingName:
60 | returnObject = item
61 | break
62 |
63 | except Exception as ex:
64 | pass
65 |
66 | ## Debug
67 | #print(returnObject)
68 |
69 | return returnObject
70 |
71 |
72 | ##
73 | ## Takes the HTTP error code passed and the message and pushes
74 | ## a message to the Slack web hook URL for our room.
75 | ##
76 | ##
77 | def sendSlackAlert (errorCode, msg):
78 | slackUrl = os.getenv('DJANGO_SLACK_ALERT_URL', '')
79 | payload = {"text": "*[PageLab] %s just happened* \n%s" % (errorCode, msg)}
80 |
81 | if slackUrl:
82 | r = requests.post(slackUrl, json=payload)
83 |
84 |
85 | ##
86 | ## Generic "send email" method, accepts three simple arguements.
87 | ## NOTE: send_mail function requires email to be array.
88 | ##
89 | ##
90 | def sendEmailNotification(sendToArr, emailTitle, emailBody):
91 | ## Debug and console print instead of actually sending while testing:
92 | #print("[FRM notification] " + emailTitle, sendToArr, emailBody)
93 | #return
94 |
95 | ## If no emails were setup or passed to us, then we can't send anything.
96 | if len(sendToArr) == 0:
97 | return;
98 |
99 | try:
100 | send_mail(
101 | "[PageLab notification] %s" % emailTitle,
102 | emailBody,
103 | "do-not-reply@fakedomain.com",
104 | sendToArr,
105 | fail_silently=True,
106 | html_message='
%s
' % emailBody
107 | )
108 | except:
109 | #TODO: LOG THIS as an error so we know if email sending is failing.
110 | pass
111 |
112 |
113 | ##
114 | ## Takes a LighthouseRun queryset and creates data object used by the line chart
115 | ## on the report detail page to chart the score history.
116 | ##
117 | ##
118 | def createHistoricalScoreChartData(LighthouseRunQueryset):
119 | ## Setup arrays of data for the line chart.
120 | ## Each object is an array that is simply passed to D3 and each represents a line on the chart.
121 | lineChartData = {
122 | 'dates': ['x'],
123 | 'perfScores': ['Performance'],
124 | 'a11yScores': ['Accessibility '],
125 | 'seoScores': ['SEO'],
126 | }
127 |
128 | ## Safety: IF there are actually any items in the inbound queryset, add them as data points.
129 | if LighthouseRunQueryset is not None and LighthouseRunQueryset.count() > 0:
130 | ## Get list of field values as array data and add to our arrays setup above for each line.
131 | lhRunsPerfScores = LighthouseRunQueryset.values_list('performance_score', flat=True)
132 | lhRunsA11yScores = LighthouseRunQueryset.values_list('accessibility_score', flat=True)
133 | lhRunsSeoScores = LighthouseRunQueryset.values_list('seo_score', flat=True)
134 |
135 | ## Add the data values array for each line we want to chart.
136 | lineChartData['perfScores'].extend(list(lhRunsPerfScores))
137 | lineChartData['a11yScores'].extend(list(lhRunsA11yScores))
138 | lineChartData['seoScores'].extend(list(lhRunsSeoScores))
139 |
140 | ## Add dates, formatted, as x-axis array data.
141 | for runData in LighthouseRunQueryset:
142 | lineChartData['dates'].append(runData.created_date.strftime('%d-%m-%Y'))
143 |
144 | ## This is the exact specific data object this chart uses.
145 | ## We just echo this out to the JS. No further processing needed.
146 | ## It's all here, nice tight bundle and makes the page JS real clean.
147 |
148 | data = {
149 | 'x': 'x',
150 | 'xFormat': '%d-%m-%Y',
151 | 'type': 'spline',
152 | 'columns': [
153 | lineChartData['dates'],
154 | lineChartData['perfScores'],
155 | lineChartData['a11yScores'],
156 | lineChartData['seoScores']
157 | ]
158 | }
159 |
160 | return data
161 |
162 |
163 | ## *** FUTURE FEATURE ***
164 | ##
165 | ## Will be used with date pickers UI to allow user to select start/stop date range
166 | ## of data they want charted and in the data table and other places.
167 | ##
168 | ##
169 | ## Takes a LighthouseRun queryset (for a given URL) and filters it to a given date scope.
170 | ## This is used in several views, so it's here. Also allows us to write a test for it.
171 | ## Use cases:
172 | ## Show chart/data with ALL lighthouse runs.
173 | ## Show chart/data with runs from the past X # days.
174 | ## Show chart/data with runs from Sept 5 to Oct 24.
175 | ## Show chart/data with runs up until Oct 16.
176 | ## Show chart/data with runs from Oct 17 and later.
177 | ##
178 | ##
179 | def lighthouseRunsByDate(LighthouseRunQueryset, startDate=None, endDate=None):
180 | if startDate is not None:
181 | LighthouseRunQueryset = LighthouseRunQueryset.filter(created_date__gt=startDate)
182 |
183 | if endDate is not None:
184 | LighthouseRunQueryset = LighthouseRunQueryset.filter(created_date__lt=endDate)
185 |
186 | return LighthouseRunQueryset
187 |
188 |
189 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/import_csv.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import datetime
3 |
4 | field_names=['url', 'url2', 'views', 'hist', 'sequence',]
5 |
6 | from django.contrib.auth.models import User
7 | from django.db import transaction
8 | from django.db.models import Avg, Max, Min, Q, Sum
9 | from django.utils import timezone
10 |
11 | from report.models import (Url, LighthouseRun, LighthouseDataRaw, LighthouseDataUsertiming,
12 | UserTimingMeasureName, UserTimingMeasure, UserTimingMeasureAverage)
13 |
14 |
15 | superuser = User.objects.get(id=1)
16 |
17 |
18 | def load_urls_into_db(path):
19 | f = open(path, 'r')
20 |
21 | reader = csv.DictReader(f,fieldnames= field_names)
22 |
23 | row_id = 0
24 |
25 | for row in reader:
26 | urlObj = Url(
27 | created_by = superuser,
28 | edited_by = superuser,
29 | url = 'https://%s' % row['url'],
30 | sequence = int(row['sequence'])
31 | )
32 | urlObj.save()
33 |
34 |
35 | def write_report_csv(path, date_since=None, include_user_timing=False):
36 | if not path:
37 | print('path is required')
38 | return
39 |
40 | file = open(path, 'w')
41 | writer = csv.writer(file)
42 | with file:
43 | writer.writerow([
44 | "test_id",
45 | "url_id",
46 | "created_date",
47 | "url",
48 | "performance_score",
49 | "total_byte_weight",
50 | "number_network_requests",
51 | "time_to_first_byte",
52 | "first_contentful_paint",
53 | "first_meaningful_paint",
54 | "dom_content_loaded",
55 | "dom_loaded",
56 | "interactive",
57 | "masthead_onscreen",
58 | "redirect_hops",
59 | "redirect_wasted_ms",
60 | "sequence",
61 | "user_timing_data"
62 | ])
63 |
64 | if date_since:
65 | if date_since['month'] and date_since['day'] and date_since['year']:
66 | m = date_since['month']
67 | d = date_since['day']
68 | y = date_since['year']
69 | run_data = LighthouseRun.objects.filter(created_date__gte=datetime.date(y, m, d))
70 | else:
71 | raise Exception('date_since requires month day and year properties')
72 | else:
73 | run_data = LighthouseRun.objects.all()
74 |
75 | for run in run_data:
76 | if include_user_timing:
77 | try:
78 | user_timing_data = str(LighthouseDataUsertiming.objects.get(lighthouse_run=run).report_data)
79 | except Exception as ex:
80 | user_timing_data = ''
81 | else:
82 | user_timing_data = ''
83 | try:
84 | writer.writerow([
85 | run.id,
86 | run.url.id,
87 | run.created_date,
88 | run.url.url,
89 | run.performance_score,
90 | run.total_byte_weight,
91 | run.number_network_requests,
92 | run.time_to_first_byte,
93 | run.first_contentful_paint,
94 | run.first_meaningful_paint,
95 | run.dom_content_loaded,
96 | run.dom_loaded,
97 | run.interactive,
98 | run.masthead_onscreen,
99 | run.redirect_hops,
100 | run.redirect_wasted_ms,
101 | run.url.id,
102 | user_timing_data
103 | ])
104 | except Exception as ex:
105 | print(ex)
106 |
107 |
108 | def update_urls(path):
109 | f = open(path, 'r')
110 | fields = ['url', 'page_compl_url',]
111 | reader = csv.DictReader(f, fieldnames=fields)
112 |
113 | for row in reader:
114 | try:
115 | urlSet = Url.objects.filter(url='https://%s' % row['url'])
116 | urlObj = urlSet.first()
117 | urlObj.save()
118 | except Exception as ex:
119 | print(ex)
120 |
121 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-10-26 16:22
2 |
3 | from django.conf import settings
4 | import django.contrib.postgres.fields.jsonb
5 | from django.db import migrations, models
6 | import django.db.models.deletion
7 |
8 |
9 | class Migration(migrations.Migration):
10 |
11 | initial = True
12 |
13 | dependencies = [
14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15 | ]
16 |
17 | operations = [
18 | migrations.CreateModel(
19 | name='BannerNotification',
20 | fields=[
21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22 | ('name', models.CharField(max_length=255)),
23 | ('active', models.BooleanField(default=False)),
24 | ('banner_text', models.CharField(max_length=255)),
25 | ('banner_type', models.CharField(choices=[('info', 'info'), ('warn', 'warn'), ('alert', 'alert')], default='info', max_length=20)),
26 | ],
27 | options={
28 | 'ordering': ['active', 'name'],
29 | },
30 | ),
31 | migrations.CreateModel(
32 | name='LighthouseDataRaw',
33 | fields=[
34 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
35 | ('created_date', models.DateTimeField(auto_now_add=True)),
36 | ('report_data', django.contrib.postgres.fields.jsonb.JSONField()),
37 | ],
38 | options={
39 | 'verbose_name_plural': 'Lighthouse data raw',
40 | },
41 | ),
42 | migrations.CreateModel(
43 | name='LighthouseDataUsertiming',
44 | fields=[
45 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
46 | ('created_date', models.DateTimeField(auto_now_add=True)),
47 | ('report_data', django.contrib.postgres.fields.jsonb.JSONField()),
48 | ],
49 | ),
50 | migrations.CreateModel(
51 | name='LighthouseRun',
52 | fields=[
53 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
54 | ('created_date', models.DateTimeField(auto_now_add=True)),
55 | ('invalid_run', models.BooleanField(default=False)),
56 | ('http_error_code', models.PositiveIntegerField(blank=True, null=True)),
57 | ('lighthouse_error_code', models.CharField(blank=True, max_length=255, null=True)),
58 | ('lighthouse_error_msg', models.TextField(blank=True, null=True)),
59 | ('accessibility_score', models.PositiveIntegerField(default=0)),
60 | ('dom_content_loaded', models.PositiveIntegerField(default=0)),
61 | ('first_contentful_paint', models.PositiveIntegerField(default=0)),
62 | ('first_meaningful_paint', models.PositiveIntegerField(default=0)),
63 | ('interactive', models.PositiveIntegerField(default=0)),
64 | ('dom_loaded', models.PositiveIntegerField(default=0)),
65 | ('masthead_onscreen', models.PositiveIntegerField(default=0)),
66 | ('number_network_requests', models.PositiveIntegerField(default=0)),
67 | ('performance_score', models.PositiveIntegerField(default=0)),
68 | ('redirect_hops', models.PositiveIntegerField(default=0)),
69 | ('redirect_wasted_ms', models.PositiveIntegerField(default=0)),
70 | ('seo_score', models.PositiveIntegerField(default=0)),
71 | ('thumbnail_image', models.TextField(blank=True, null=True)),
72 | ('time_to_first_byte', models.PositiveIntegerField(default=0)),
73 | ('total_byte_weight', models.PositiveIntegerField(default=0)),
74 | ],
75 | options={
76 | 'ordering': ['-created_date'],
77 | },
78 | ),
79 | migrations.CreateModel(
80 | name='PageView',
81 | fields=[
82 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
83 | ('created_date', models.DateTimeField(auto_now_add=True)),
84 | ('modified_date', models.DateTimeField(auto_now=True)),
85 | ('url', models.CharField(max_length=2000, unique=True)),
86 | ('view_count', models.PositiveIntegerField(default=0)),
87 | ],
88 | options={
89 | 'ordering': ['-view_count'],
90 | },
91 | ),
92 | migrations.CreateModel(
93 | name='Url',
94 | fields=[
95 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
96 | ('created_date', models.DateTimeField(auto_now_add=True)),
97 | ('edited_date', models.DateTimeField(auto_now=True)),
98 | ('url', models.URLField(unique=True)),
99 | ('inactive', models.BooleanField(default=False)),
100 | ('sequence', models.PositiveIntegerField(default=0)),
101 | ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='url_created_by', to=settings.AUTH_USER_MODEL)),
102 | ('edited_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='url_edited_by', to=settings.AUTH_USER_MODEL)),
103 | ('lighthouse_run', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='url_lighthouse_run', to='report.LighthouseRun')),
104 | ],
105 | options={
106 | 'ordering': ['url'],
107 | },
108 | ),
109 | migrations.CreateModel(
110 | name='UrlKpiAverage',
111 | fields=[
112 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
113 | ('created_date', models.DateTimeField(auto_now_add=True)),
114 | ('number_samples', models.PositiveIntegerField(default=0)),
115 | ('invalid_average', models.BooleanField(default=False)),
116 | ('accessibility_score', models.PositiveIntegerField(default=0)),
117 | ('dom_content_loaded', models.PositiveIntegerField(default=0)),
118 | ('first_contentful_paint', models.PositiveIntegerField(default=0)),
119 | ('first_meaningful_paint', models.PositiveIntegerField(default=0)),
120 | ('interactive', models.PositiveIntegerField(default=0)),
121 | ('dom_loaded', models.PositiveIntegerField(default=0)),
122 | ('masthead_onscreen', models.PositiveIntegerField(default=0)),
123 | ('number_network_requests', models.PositiveIntegerField(default=0)),
124 | ('performance_score', models.PositiveIntegerField(default=0)),
125 | ('redirect_wasted_ms', models.PositiveIntegerField(default=0)),
126 | ('seo_score', models.PositiveIntegerField(default=0)),
127 | ('time_to_first_byte', models.PositiveIntegerField(default=0)),
128 | ('total_byte_weight', models.PositiveIntegerField(default=0)),
129 | ('url', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='url_kpi_average_url', to='report.Url')),
130 | ],
131 | ),
132 | migrations.AddField(
133 | model_name='url',
134 | name='url_kpi_average',
135 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='url_url_kpi_average', to='report.UrlKpiAverage'),
136 | ),
137 | migrations.AddField(
138 | model_name='lighthouserun',
139 | name='url',
140 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lighthouse_run_url', to='report.Url'),
141 | ),
142 | migrations.AddField(
143 | model_name='lighthousedatausertiming',
144 | name='lighthouse_run',
145 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lighthouse_data_usertiming_lighthouse_run', to='report.LighthouseRun'),
146 | ),
147 | migrations.AddField(
148 | model_name='lighthousedataraw',
149 | name='lighthouse_run',
150 | field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='lighthouse_data_raw_lighthouse_run', to='report.LighthouseRun'),
151 | ),
152 | ]
153 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0002_auto_20181026_1224.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-10-26 16:24
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('report', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='UserTimingMeasure',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('created_date', models.DateTimeField(auto_now_add=True)),
19 | ('duration', models.PositiveIntegerField(default=0)),
20 | ('start_time', models.PositiveIntegerField(default=0)),
21 | ('lighthouse_run', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_timing_measure_lighthouse_run', to='report.LighthouseRun')),
22 | ],
23 | options={
24 | 'ordering': ['start_time'],
25 | },
26 | ),
27 | migrations.CreateModel(
28 | name='UserTimingMeasureAverage',
29 | fields=[
30 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31 | ('created_date', models.DateTimeField(auto_now_add=True)),
32 | ('duration', models.PositiveIntegerField(default=0)),
33 | ('start_time', models.PositiveIntegerField(default=0)),
34 | ('number_samples', models.PositiveIntegerField(default=0)),
35 | ],
36 | options={
37 | 'ordering': ['name'],
38 | },
39 | ),
40 | migrations.CreateModel(
41 | name='UserTimingMeasureName',
42 | fields=[
43 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
44 | ('created_date', models.DateTimeField(auto_now_add=True)),
45 | ('name', models.CharField(max_length=255)),
46 | ],
47 | options={
48 | 'ordering': ['name'],
49 | },
50 | ),
51 | migrations.AddField(
52 | model_name='urlkpiaverage',
53 | name='redirect_hops',
54 | field=models.PositiveIntegerField(default=0),
55 | ),
56 | migrations.AddField(
57 | model_name='usertimingmeasureaverage',
58 | name='name',
59 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_timing_measure_avg_name', to='report.UserTimingMeasureName'),
60 | ),
61 | migrations.AddField(
62 | model_name='usertimingmeasureaverage',
63 | name='url',
64 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_timing_measure_avg_url', to='report.Url'),
65 | ),
66 | migrations.AddField(
67 | model_name='usertimingmeasure',
68 | name='name',
69 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_timing_measure_name', to='report.UserTimingMeasureName'),
70 | ),
71 | migrations.AddField(
72 | model_name='usertimingmeasure',
73 | name='url',
74 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_timing_measure_url', to='report.Url'),
75 | ),
76 | ]
77 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0003_auto_20181026_1558.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-10-26 19:58
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0002_auto_20181026_1224'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='usertimingmeasureaverage',
15 | options={'ordering': ['start_time']},
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0004_url_user_timings_migrated.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0 on 2018-10-30 01:14
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0003_auto_20181026_1558'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='url',
15 | name='user_timings_migrated',
16 | field=models.DateTimeField(null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0005_remove_url_user_timings_migrated.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-02 02:52
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0004_url_user_timings_migrated'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='url',
15 | name='user_timings_migrated',
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0006_auto_20181107_1651.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-07 21:51
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('report', '0005_remove_url_user_timings_migrated'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='SearchKeyVal',
16 | fields=[
17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18 | ('created_date', models.DateTimeField(auto_now_add=True)),
19 | ('key', models.CharField(max_length=128)),
20 | ('val', models.CharField(blank=True, max_length=255, null=True)),
21 | ],
22 | options={
23 | 'ordering': ['key'],
24 | },
25 | ),
26 | migrations.CreateModel(
27 | name='UrlFilter',
28 | fields=[
29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
30 | ('created_date', models.DateTimeField(auto_now_add=True)),
31 | ('modified_date', models.DateTimeField(auto_now=True)),
32 | ('name', models.CharField(max_length=255)),
33 | ('description', models.TextField(blank=True, null=True)),
34 | ],
35 | options={
36 | 'ordering': ['name'],
37 | },
38 | ),
39 | migrations.CreateModel(
40 | name='UrlFilterPart',
41 | fields=[
42 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
43 | ('prop', models.CharField(choices=[('protocol', 'protocol'), ('host', 'host'), ('hostname', 'hostname'), ('port', 'port'), ('pathname', 'pathname'), ('path_segment', 'path_segment'), ('search', 'search'), ('search_key', 'search_key'), ('hash', 'hash'), ('origin', 'origin')], max_length=16)),
44 | ('filter_key', models.CharField(blank=True, max_length=128, null=True)),
45 | ('filter_val', models.CharField(max_length=128)),
46 | ('url_filter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='url_filter_part_url_filter', to='report.UrlFilter')),
47 | ],
48 | options={
49 | 'ordering': ['prop'],
50 | },
51 | ),
52 | migrations.CreateModel(
53 | name='UrlOwner',
54 | fields=[
55 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
56 | ('created_date', models.DateTimeField(auto_now_add=True)),
57 | ('modified_date', models.DateTimeField(auto_now=True)),
58 | ('owner_name', models.CharField(max_length=64)),
59 | ('owner_email', models.EmailField(blank=True, max_length=254, null=True)),
60 | ('owner_description', models.TextField(blank=True, null=True)),
61 | ('owner_homepage', models.URLField(blank=True, null=True)),
62 | ],
63 | options={
64 | 'ordering': ['owner_name'],
65 | },
66 | ),
67 | migrations.CreateModel(
68 | name='UrlPath',
69 | fields=[
70 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
71 | ('created_date', models.DateTimeField(auto_now_add=True)),
72 | ('sequence', models.IntegerField(default=0)),
73 | ('path', models.CharField(max_length=255)),
74 | ],
75 | options={
76 | 'ordering': ['sequence'],
77 | },
78 | ),
79 | migrations.AddField(
80 | model_name='url',
81 | name='hash',
82 | field=models.CharField(blank=True, max_length=255, null=True),
83 | ),
84 | migrations.AddField(
85 | model_name='url',
86 | name='host',
87 | field=models.CharField(default='https://ibm.com', max_length=255),
88 | preserve_default=False,
89 | ),
90 | migrations.AddField(
91 | model_name='url',
92 | name='hostname',
93 | field=models.CharField(default='https://ibm.com', max_length=255),
94 | preserve_default=False,
95 | ),
96 | migrations.AddField(
97 | model_name='url',
98 | name='origin',
99 | field=models.CharField(default='https://ibm.com', max_length=255),
100 | preserve_default=False,
101 | ),
102 | migrations.AddField(
103 | model_name='url',
104 | name='parsed_url',
105 | field=models.URLField(default='https://ibm.com'),
106 | preserve_default=False,
107 | ),
108 | migrations.AddField(
109 | model_name='url',
110 | name='pathname',
111 | field=models.CharField(default='', max_length=255),
112 | preserve_default=False,
113 | ),
114 | migrations.AddField(
115 | model_name='url',
116 | name='port',
117 | field=models.IntegerField(blank=True, default=None, null=True),
118 | ),
119 | migrations.AddField(
120 | model_name='url',
121 | name='protocol',
122 | field=models.CharField(default='https', max_length=8),
123 | preserve_default=False,
124 | ),
125 | migrations.AddField(
126 | model_name='url',
127 | name='search',
128 | field=models.CharField(blank=True, max_length=255, null=True),
129 | ),
130 | migrations.AddField(
131 | model_name='urlpath',
132 | name='url',
133 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='url_path_url', to='report.Url'),
134 | ),
135 | migrations.AddField(
136 | model_name='searchkeyval',
137 | name='url',
138 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='search_key_val_url', to='report.Url'),
139 | ),
140 | migrations.AddField(
141 | model_name='url',
142 | name='owner',
143 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='url_owner_url', to='report.UrlOwner'),
144 | ),
145 | ]
146 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0007_auto_20181107_1652.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-07 21:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0006_auto_20181107_1651'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name='url',
15 | name='pathname',
16 | field=models.CharField(blank=True, max_length=255, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0008_auto_20181108_0952.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-08 14:52
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0007_auto_20181107_1652'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='urlfilterpart',
15 | name='filter_path_index',
16 | field=models.IntegerField(blank=True, null=True),
17 | ),
18 | migrations.AlterField(
19 | model_name='url',
20 | name='host',
21 | field=models.CharField(blank=True, max_length=255),
22 | ),
23 | migrations.AlterField(
24 | model_name='url',
25 | name='hostname',
26 | field=models.CharField(blank=True, max_length=255),
27 | ),
28 | migrations.AlterField(
29 | model_name='url',
30 | name='origin',
31 | field=models.CharField(blank=True, max_length=255),
32 | ),
33 | migrations.AlterField(
34 | model_name='url',
35 | name='parsed_url',
36 | field=models.URLField(blank=True),
37 | ),
38 | migrations.AlterField(
39 | model_name='url',
40 | name='protocol',
41 | field=models.CharField(blank=True, max_length=8),
42 | ),
43 | migrations.AlterField(
44 | model_name='urlfilter',
45 | name='name',
46 | field=models.CharField(max_length=255, unique=True),
47 | ),
48 | ]
49 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0009_auto_20181108_1041.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-08 15:41
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0008_auto_20181108_0952'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='searchkeyval',
15 | name='url',
16 | ),
17 | migrations.RemoveField(
18 | model_name='urlpath',
19 | name='url',
20 | ),
21 | migrations.AddField(
22 | model_name='url',
23 | name='search_key_vals',
24 | field=models.ManyToManyField(to='report.SearchKeyVal'),
25 | ),
26 | migrations.AddField(
27 | model_name='url',
28 | name='url_paths',
29 | field=models.ManyToManyField(to='report.UrlPath'),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0010_auto_20181108_1413.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-08 19:13
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('report', '0009_auto_20181108_1041'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='url',
16 | name='search_key_vals',
17 | ),
18 | migrations.RemoveField(
19 | model_name='url',
20 | name='url_paths',
21 | ),
22 | migrations.AddField(
23 | model_name='searchkeyval',
24 | name='url',
25 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='search_key_val_url', to='report.Url'),
26 | preserve_default=False,
27 | ),
28 | migrations.AddField(
29 | model_name='urlpath',
30 | name='url',
31 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='url_path_url', to='report.Url'),
32 | preserve_default=False,
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0011_auto_20181108_1417.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-08 19:17
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0010_auto_20181108_1413'),
10 | ]
11 |
12 | operations = [
13 | migrations.RemoveField(
14 | model_name='searchkeyval',
15 | name='url',
16 | ),
17 | migrations.RemoveField(
18 | model_name='urlpath',
19 | name='url',
20 | ),
21 | migrations.AddField(
22 | model_name='url',
23 | name='search_key_vals',
24 | field=models.ManyToManyField(blank=True, null=True, to='report.SearchKeyVal'),
25 | ),
26 | migrations.AddField(
27 | model_name='url',
28 | name='url_paths',
29 | field=models.ManyToManyField(blank=True, null=True, to='report.UrlPath'),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0012_auto_20181109_1225.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-09 17:25
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0011_auto_20181108_1417'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='urlfilter',
15 | name='slug',
16 | field=models.SlugField(default='placeholder'),
17 | preserve_default=False,
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0012_auto_20181113_1510.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-13 20:10
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0011_auto_20181108_1417'),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name='urlpath',
15 | options={'ordering': ['path']},
16 | ),
17 | migrations.AlterField(
18 | model_name='url',
19 | name='search_key_vals',
20 | field=models.ManyToManyField(blank=True, to='report.SearchKeyVal'),
21 | ),
22 | migrations.AlterField(
23 | model_name='url',
24 | name='url_paths',
25 | field=models.ManyToManyField(blank=True, to='report.UrlPath'),
26 | ),
27 | migrations.AlterField(
28 | model_name='urlpath',
29 | name='sequence',
30 | field=models.IntegerField(blank=True, default=0, null=True),
31 | ),
32 | ]
33 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0013_auto_20181129_1047.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-29 15:47
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0012_auto_20181113_1510'),
10 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Team',
15 | fields=[
16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17 | ('created_date', models.DateTimeField(auto_now_add=True)),
18 | ('name', models.CharField(max_length=255)),
19 | ('description', models.TextField(blank=True, null=True)),
20 | ],
21 | options={
22 | 'ordering': ['name'],
23 | },
24 | ),
25 | migrations.AddField(
26 | model_name='usertimingmeasurename',
27 | name='description',
28 | field=models.TextField(blank=True, null=True),
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0014_usertimingmeasurename_team.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-29 15:49
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('report', '0013_auto_20181129_1047'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='usertimingmeasurename',
16 | name='team',
17 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='user_timing_measure_name_team', to='report.Team'),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/0015_auto_20181130_1123.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.0.8 on 2018-11-30 16:23
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('report', '0014_usertimingmeasurename_team'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddIndex(
14 | model_name='usertimingmeasureaverage',
15 | index=models.Index(fields=['created_date', 'url', 'name'], name='report_user_created_e48272_idx'),
16 | ),
17 | migrations.AddIndex(
18 | model_name='usertimingmeasure',
19 | index=models.Index(fields=['created_date', 'url', 'name'], name='report_user_created_9a13d5_idx'),
20 | ),
21 | migrations.AddIndex(
22 | model_name='lighthouserun',
23 | index=models.Index(fields=['created_date'], name='report_ligh_created_86d861_idx'),
24 | ),
25 | migrations.AddIndex(
26 | model_name='lighthouserun',
27 | index=models.Index(fields=['url'], name='report_ligh_url_id_7cba93_idx'),
28 | ),
29 | migrations.AddIndex(
30 | model_name='urlpath',
31 | index=models.Index(fields=['path', 'sequence'], name='report_urlp_path_94a9ef_idx'),
32 | ),
33 | migrations.AddIndex(
34 | model_name='url',
35 | index=models.Index(fields=['url'], name='report_url_url_d2ecf9_idx'),
36 | ),
37 | migrations.AddIndex(
38 | model_name='lighthousedataraw',
39 | index=models.Index(fields=['created_date', 'lighthouse_run'], name='report_ligh_created_df2ed1_idx'),
40 | ),
41 | migrations.AddIndex(
42 | model_name='urlkpiaverage',
43 | index=models.Index(fields=['created_date', 'url'], name='report_urlk_created_51bf7a_idx'),
44 | ),
45 | migrations.AddIndex(
46 | model_name='searchkeyval',
47 | index=models.Index(fields=['key', 'val'], name='report_sear_key_8430fe_idx'),
48 | ),
49 | migrations.AddIndex(
50 | model_name='urlfilterpart',
51 | index=models.Index(fields=['prop', 'filter_key', 'filter_path_index', 'filter_val'], name='report_urlf_prop_e995d7_idx'),
52 | ),
53 | migrations.AddIndex(
54 | model_name='usertimingmeasurename',
55 | index=models.Index(fields=['created_date', 'name'], name='report_user_created_3ee64b_idx'),
56 | ),
57 | migrations.AddIndex(
58 | model_name='lighthousedatausertiming',
59 | index=models.Index(fields=['created_date', 'lighthouse_run'], name='report_ligh_created_118b5d_idx'),
60 | ),
61 | ]
62 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/report/migrations/__init__.py
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% block pageTitle %} Lost and (not) found {% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Woops, whatever you were looking for isn't here. Try going back or starting from the home page .
9 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% block pageTitle %} Woops, now you've done it{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
Uh oh! A 500 error just occurred. Something on this page caused a boo-boo and it can't be rendered.
9 | Don't worry, the admins have just been notified.
10 |
11 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/home.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% load beautify %}
4 | {% load compress %}
5 | {% load static %}
6 |
7 |
8 | {% block menuHomeClass %}pl-highlight{% endblock %}
9 |
10 |
11 | {% block extraFiles %}
12 |
13 | {% compress js %}
14 |
15 | {% endcompress %}
16 |
17 | {% endblock %}
18 |
19 |
20 | {% block leadspace %}
21 |
22 |
PageLab
23 |
Automated controlled test reports providing insights for effective user experiences.
24 |
25 | {% endblock %}
26 |
27 |
28 | {% block content %}
29 |
30 |
44 |
45 | {% include "partials/compare_tray.html" %}
46 |
47 |
48 | {% endblock %}
49 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/page_template.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load bannernotification %}
3 | {% load compress %}
4 | {% load define %}
5 | {% load flatpages %}
6 | {% load flatpages_nav_highlight %}
7 | {% load pageview %}
8 | {% load template_helpers %}
9 |
10 | {% trackPageView %}
11 | {% getTemplateHelpers as templateHelpers %}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {% block browserTitle %}PageLab {% endblock %}
42 |
43 |
44 | {% compress css %}
45 |
46 |
47 |
48 |
49 | {% endcompress %}
50 |
51 | {% compress js %}
52 |
68 |
69 |
70 |
71 |
72 |
73 | {% endcompress %}
74 |
75 | {% block extraFiles %} {% endblock %}
76 |
77 |
78 |
79 |
80 |
81 |
82 | {% bannerNotification %}
83 |
84 |
85 |
86 |
87 | PageLab
88 |
89 |
90 |
91 |
92 |
93 | {% get_flatpages as flatpages %}
94 | {% for page in flatpages %}
95 | {% highlight_nav_item page.url as highlight_flag %}
96 | {{ page.title }}
97 | {% endfor %}
98 |
99 |
100 | {% block leadspace %}
101 |
102 |
{% block pageTitle %}PageLab{% endblock %}
103 | {% block pageSubtitle %}{% endblock %}
104 |
105 | {% endblock %}
106 |
107 |
108 | {% if request.session.showMessage %}
109 | {{ request.session.showMessage.text|safe }}
110 | {% endif %}
111 |
112 |
113 | {% block content %} {% endblock %}
114 |
115 |
116 | {% define "40" as imgsize %}
117 |
118 |
Made with
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/page_template_flatpages.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% block browserTitle %} {{ block.super }} - {{ flatpage.title }}{% endblock %}
4 |
5 | {% block pageTitle %}
6 |
7 | {{ flatpage.title }}
8 |
9 | {% endblock %}
10 |
11 |
12 | {% block content %}
13 |
14 | {{ flatpage.content }}
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/audit_score_donut.html:
--------------------------------------------------------------------------------
1 | {% load scoredisplayoptions %}
2 |
3 | {{ scoreValue }}
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/auth_form.html:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/banner_notification.html:
--------------------------------------------------------------------------------
1 | {% for banner in banners %}
2 |
3 | {% if banner.banner_type == "info" %}
4 |
5 |
6 |
{{ banner.banner_text }}
7 |
8 |
9 | {% elif banner.banner_type == "warn" %}
10 |
11 |
12 |
{{ banner.banner_text }}
13 |
14 |
15 | {% elif banner.banner_type == "alert" %}
16 |
17 |
18 |
{{ banner.banner_text }}
19 |
20 |
21 | {% endif %}
22 |
23 |
24 | {% endfor %}
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/compare_item.html:
--------------------------------------------------------------------------------
1 | {% load static %}
2 | {% load beautify %}
3 | {% load scoredisplayoptions %}
4 |
5 |
6 |
Remove
7 |
8 |
{{ url.url|noprotocol }}
9 |
10 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/compare_tray.html:
--------------------------------------------------------------------------------
1 | {% load beautify %}
2 | {% load static %}
3 |
4 |
5 |
6 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/home_load_items.html:
--------------------------------------------------------------------------------
1 | {% for url in urls %}
2 | {% include "partials/report_card.html" with item=url %}
3 | {% endfor %}
4 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/modal.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% if modalTitle %}
6 | {% block modalTitle %}{% endblock %}
7 | {% endif %}
8 |
9 |
10 |
{% block modalContent %}{% endblock %}
11 |
12 |
13 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/modal_redirects.html:
--------------------------------------------------------------------------------
1 | {% load beautify %}
2 |
3 |
4 |
5 |
6 |
7 | Redirect hops
8 |
9 |
10 |
11 |
12 |
13 |
14 | Wasted MS
15 | Url
16 |
18 |
19 | {% for redirect in redirects %}
20 |
21 | {{ redirect.wastedMs|withComma }}
22 | {{ redirect.url }}
23 |
24 | {% endfor %}
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/report_card.html:
--------------------------------------------------------------------------------
1 | {% load define %}
2 | {% load static %}
3 | {% load beautify %}
4 | {% load scoredisplayoptions %}
5 |
6 |
7 |
40 |
41 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/report_detail_table_run_kpi_rows.html:
--------------------------------------------------------------------------------
1 | {% load beautify %}
2 | {% load template_helpers %}
3 |
4 | {% getTemplateHelpers as templateHelpers %}
5 |
6 |
7 | {% for lighthouseRun in lighthouseRuns %}
8 |
9 | {{ forloop.counter }}
10 | {{ templateHelpers.html.icons.newWindow|safe }}
11 | {{ lighthouseRun.created_date }}
12 | {{ lighthouseRun.performance_score }}
13 | {{ lighthouseRun.accessibility_score }}
14 | {{ lighthouseRun.seo_score }}
15 | {{ lighthouseRun.total_byte_weight|byteToKb }}
16 | {{ lighthouseRun.number_network_requests }}
17 | {{ lighthouseRun.time_to_first_byte }}
18 | {{ lighthouseRun.dom_content_loaded }}
19 | {{ lighthouseRun.first_contentful_paint }}
20 | {{ lighthouseRun.first_meaningful_paint }}
21 | {{ lighthouseRun.interactive }}
22 | {{ lighthouseRun.dom_loaded }}
23 | {{ lighthouseRun.redirect_hops }}
24 | {{ lighthouseRun.redirect_wasted_ms }}
25 |
26 | {% endfor %}
27 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/url_filter_select.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
URL Filter:
5 |
6 |
7 | None
8 |
9 | {% for url_filter in filters %}
10 | {{ url_filter.name }}
11 | {% endfor %}
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/partials/urls_compare_2_table.html:
--------------------------------------------------------------------------------
1 | {% load scoredisplayoptions %}
2 | {% load beautify %}
3 | {% load static %}
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ url1.url|noprotocol }} {{ templateHelpers.html.icons.newWindow|safe }}
16 | {{ url2.url|noprotocol }} {{ templateHelpers.html.icons.newWindow|safe }}
17 |
18 |
19 |
20 | Last tested:
21 | {{ url1.lighthouse_run.created_date }}
22 | {{ url2.lighthouse_run.created_date }}
23 |
24 |
25 |
26 |
27 | URL KPI averages
28 | URL KPI averages
29 |
30 |
31 |
32 | Performance score:
33 | {% include "partials/audit_score_donut.html" with scoreValue=url1.url_kpi_average.performance_score %}
34 | {% include "partials/audit_score_donut.html" with scoreValue=url2.url_kpi_average.performance_score %}
35 |
36 |
37 |
38 | Accessibility score:
39 | {% include "partials/audit_score_donut.html" with scoreValue=url1.url_kpi_average.accessibility_score %}
40 | {% include "partials/audit_score_donut.html" with scoreValue=url2.url_kpi_average.accessibility_score %}
41 |
42 |
43 |
44 | SEO score:
45 | {% include "partials/audit_score_donut.html" with scoreValue=url1.url_kpi_average.seo_score %}
46 | {% include "partials/audit_score_donut.html" with scoreValue=url2.url_kpi_average.seo_score %}
47 |
48 |
49 |
50 | Total size:
51 | {{ url1.url_kpi_average.total_byte_weight|kbToMb }}
52 | {{ url2.url_kpi_average.total_byte_weight|kbToMb }}
53 |
54 |
55 |
56 | # of network requests:
57 | {{ url1.url_kpi_average.number_network_requests|withComma }}
58 | {{ url2.url_kpi_average.number_network_requests|withComma }}
59 |
60 |
61 |
62 | Time to first byte:
63 | {{ url1.url_kpi_average.time_to_first_byte|withComma }} ms
64 | {{ url2.url_kpi_average.time_to_first_byte|withComma }} ms
65 |
66 |
67 |
68 | DOM content loaded:
69 | {{ url1.url_kpi_average.dom_content_loaded|withComma }} ms
70 | {{ url2.url_kpi_average.dom_content_loaded|withComma }} ms
71 |
72 |
73 |
74 | Masthead onscreen:
75 | {% if url1.url_kpi_average.masthead_onscreen != 0 %}
76 | {{ url1.url_kpi_average.masthead_onscreen|withComma }} ms
77 | {% else %}
78 | N/A
79 | {% endif %}
80 |
81 | {% if url2.url_kpi_average.masthead_onscreen != 0 %}
82 | {{ url2.url_kpi_average.masthead_onscreen|withComma }} ms
83 | {% else %}
84 | N/A
85 | {% endif %}
86 |
87 |
88 |
89 |
90 | DOM loaded:
91 | {{ url1.url_kpi_average.dom_loaded|withComma }} ms
92 | {{ url2.url_kpi_average.dom_loaded|withComma }} ms
93 |
94 |
95 |
96 | First contentful paint:
97 | {{ url1.url_kpi_average.first_contentful_paint|withComma }} ms
98 | {{ url2.url_kpi_average.first_contentful_paint|withComma }} ms
99 |
100 |
101 |
102 | First meaningful paint:
103 | {{ url1.url_kpi_average.first_meaningful_paint|withComma }} ms
104 | {{ url2.url_kpi_average.first_meaningful_paint|withComma }} ms
105 |
106 |
107 |
108 | Fully interactive:
109 | {{ url1.url_kpi_average.interactive|withComma }} ms
110 | {{ url2.url_kpi_average.interactive|withComma }} ms
111 |
112 |
113 |
114 | # of redirects:
115 | {{ url1.lighthouse_run.redirect_hops|withComma }}
116 | {{ url2.lighthouse_run.redirect_hops|withComma }}
117 |
118 |
119 |
120 | Wasted redirect time:
121 | {{ url1.url_kpi_average.redirect_wasted_ms|withComma }} ms
122 | {{ url2.url_kpi_average.redirect_wasted_ms|withComma }} ms
123 |
124 |
125 |
126 | Full report record {{ templateHelpers.html.icons.chevronForward|safe }}
127 | Full report record {{ templateHelpers.html.icons.chevronForward|safe }}
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/reports_browse.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% load beautify %}
4 | {% load compress %}
5 | {% load static %}
6 |
7 |
8 | {% block browserTitle %} {{ block.super }} - Browse reports{% endblock %}
9 |
10 | {% block pageTitle %} Report cards {% endblock %}
11 |
12 | {% block menuBrowsereportsClass %}pl-highlight{% endblock %}
13 |
14 |
15 | {% block extraFiles %}
16 |
17 | {# Already compressed version, no point. #}
18 | {# If we add select list on any other page, move select2 JS/CSS to common template include compress. #}
19 |
20 |
21 | {# Compress all this into 1 included compressed cached file. #}
22 | {% compress js %}
23 |
24 |
25 |
94 | {% endcompress %}
95 |
96 | {% endblock %}
97 |
98 |
99 | {% block content %}
100 |
101 |
143 |
144 |
145 |
146 | {% for url in urls %}
147 | {% include "partials/report_card.html" with url=url %}
148 | {% endfor %}
149 |
150 |
151 |
152 | {% if hasNextPage %}
153 |
154 | Load more
155 |
156 | {% endif %}
157 |
158 | {% include "partials/compare_tray.html" %}
159 |
160 |
161 | {% endblock %}
162 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/reports_filters.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 |
4 | {% block browserTitle %} {{ block.super }} - Filters{% endblock %}
5 |
6 | {% block pageTitle %} Filters {% endblock %}
7 |
8 | {% block menuFiltersClass %}pl-highlight{% endblock %}
9 |
10 |
11 | {% block extraFiles %}
12 |
13 |
36 |
37 |
38 |
63 |
64 | {% endblock %}
65 |
66 | {% block content %}
67 |
68 |
69 |
70 |
Filters are used to scope the reports list page and dashboard view. They allow you to easily view a particular subset of URLs.
71 |
72 | {% if filter_sets.count == 0 %}
73 |
74 |
There are currently no filter sets. Contact an admin if you would like to have one created for you.
75 |
76 | {% else %}
77 |
78 |
104 |
105 | {% endif %}
106 |
107 |
108 |
109 |
110 | {% endblock %}
111 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/reports_lighthouse_viewer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PageLab - Lighthouse report viewer
6 |
7 |
8 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/reports_urls_compare.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% load beautify %}
4 | {% load static %}
5 |
6 |
7 | {% block browserTitle %} {{ block.super }} - URL report comparison {% endblock %}
8 |
9 | {% block pageTitle %} URL report comparison {% endblock %}
10 |
11 | {% block pageSubtitle %}{% endblock %}
12 |
13 | {% block leadspaceCss %}{% endblock %}
14 |
15 |
16 | {% block content %}
17 |
18 |
19 | {% if not url3 %}
20 | {% include "partials/urls_compare_2_table.html" %}
21 | {% else %}
22 | {% include "partials/urls_compare_3_table.html" %}
23 | {% endif %}
24 |
25 |
26 | {% include "partials/compare_tray.html" %}
27 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/reports_urls_detail_noruns.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% load beautify %}
4 | {% load static %}
5 |
6 |
7 | {% block browserTitle %} {{ block.super }} - URL report {% endblock %}
8 |
9 | {% block extraFiles %}{% endblock %}
10 |
11 | {% block pageTitle %} URL report {% endblock %}
12 |
13 | {% block pageSubtitle %}
14 | {{ url1.url|noprotocol }}
15 | {% endblock %}
16 |
17 |
18 | {% block content %}
19 |
20 |
25 |
26 | {% include "partials/compare_tray.html" %}
27 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/signedout.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 |
4 | {% block browserTitle %} {{ block.super }} - Signed out {% endblock %}
5 |
6 | {% block pageTitle %} Signed out {% endblock %}
7 |
8 | {% block content %}
9 |
10 |
11 |
You have been signed out successfully.
12 |
13 |
14 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templates/signin.html:
--------------------------------------------------------------------------------
1 | {% extends "page_template.html" %}
2 |
3 | {% block browserTitle %} {{ block.super }} - Sign in {% endblock %}
4 |
5 | {% block pageTitle %} Sign in {% endblock %}
6 |
7 | {% block content %}
8 |
9 |
10 | {% if error %}
11 |
{{ error|safe }}
12 | {% endif %}
13 |
14 | {% include "partials/auth_form.html" %}
15 |
16 |
17 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templatetags/bannernotification.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | from report.models import BannerNotification
4 |
5 | register = template.Library()
6 |
7 |
8 | ##
9 | ## Gets all active banners and displays them at page top, using the 'banner_notification.html' template.
10 | ##
11 | @register.inclusion_tag("partials/banner_notification.html")
12 | def bannerNotification():
13 | return {"banners": BannerNotification.objects.filter(active=True)}
14 |
15 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templatetags/beautify.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from django import template
4 | from django.contrib.humanize.templatetags.humanize import intcomma
5 |
6 | register = template.Library()
7 |
8 |
9 |
10 | ##
11 | ## Strips protocol off URL for nice display/hotlink text.
12 | ##
13 | ## INPUT: http(s)://www.someDomain.com/some/path/here/
14 | ## RETURNS: www.someDomain.com/some/path/here/
15 | ##
16 | ##
17 | @register.filter
18 | def noprotocol(fullUrl):
19 | returnData = re.sub(r"https?://", "", fullUrl)
20 | returnData = re.sub(r"/$", "", returnData)
21 |
22 | return returnData
23 |
24 |
25 | ##
26 | ## Changes decimal # to whole #s, rounds it to whole num.
27 | ## Used if converting lighthouse perf #s to percents.
28 | ##
29 | ## INPUT: .83
30 | ## RETURNS: 83
31 | ##
32 | ##
33 | @register.filter
34 | def toPercent(num):
35 | try:
36 | return round(float(num) * 100)
37 | except Exception as ex:
38 | return 0
39 |
40 |
41 | ##
42 | ## Adds comma to a #.
43 | ##
44 | ## INPUT: 1529
45 | ## RETURNS: 1,529
46 | ##
47 | ##
48 | @register.filter
49 | def withComma(num):
50 | try:
51 | num = round(float(num))
52 | returnData = "%s" % (intcomma(int(num)))
53 | except Exception as ex:
54 | returnData = "0"
55 |
56 | return returnData
57 |
58 | ##
59 | ## Takes bytes and converts to nice KB or MB
60 | ##
61 | ## INPUT: 1529392
62 | ## RETURNS: 1,529 mb
63 | ##
64 | ##
65 | @register.filter
66 | def kbToMb(num):
67 | returnData = num
68 | unit = "kb"
69 |
70 | try:
71 | if int(float(num)+1) > 999999:
72 | num = round((float(num)/1000000),2)
73 | unit = "mb"
74 | else:
75 | num = round((float(num)/1000))
76 | except Exception as ex:
77 | num = 0
78 | print("|%s| is not a num?" % num)
79 |
80 | returnData = "%s %s" % (intcomma(num), unit)
81 |
82 | return returnData
83 |
84 |
85 | ##
86 | ## Takes bytes and converts to KB # only
87 | ##
88 | ## INPUT: 752392
89 | ## RETURNS: 752
90 | ##
91 | ##
92 | @register.filter
93 | def byteToKb(num):
94 | try:
95 | return round((float(num)/1000))
96 | except Exception as ex:
97 | return 0
98 |
99 |
100 | ## TODO: Santelia
101 | ##
102 | ## Truncates the middle of the URL with elipsis
103 | ##
104 | ## INPUT: http(s)://www.someDomain.com/some/really/long/path/here/
105 | ## RETURNS: www.someDomain.com/some/...here/
106 | ##
107 | ##
108 | @register.filter
109 | def truncateurl(fullUrl):
110 | returnData = fullUrl
111 |
112 | return returnData
113 |
114 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templatetags/define.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | register = template.Library()
4 |
5 |
6 | ##
7 | ## Allows you to set variables in a template.
8 | ## Used when deciding what class to use based on an object value.
9 | ##
10 | ## Usage:
11 | ##
12 | ## {% define "Edit" as action %}
13 | ##
14 | ## {{ action }}
15 | ##
16 | @register.simple_tag
17 | def define(val=None):
18 | return val
--------------------------------------------------------------------------------
/admin/pageaudit/report/templatetags/flatpages_nav_highlight.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from django.conf import settings
3 |
4 | register = template.Library()
5 |
6 | # settings value
7 | @register.simple_tag(takes_context=True)
8 | def highlight_nav_item(context, url):
9 | isMatch = False
10 |
11 | scriptPrefix = getattr(settings, "FORCE_SCRIPT_NAME", "")
12 | requestPath = context['request'].path
13 | thisPagePath = "%s%s%s" % (scriptPrefix, "/report/pages", url)
14 |
15 | if requestPath == thisPagePath:
16 | isMatch = True
17 |
18 | return isMatch
19 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templatetags/pageview.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | from report.models import PageView
4 |
5 | register = template.Library()
6 |
7 | ##
8 | ## On page template, increments the hit count to the URL, or creates it as 1.
9 | ##
10 | @register.simple_tag(takes_context=True)
11 | def trackPageView(context):
12 | request = context['request']
13 |
14 | try:
15 | pvObj = PageView.objects.get(url=request.path)
16 | pvObj.view_count = pvObj.view_count + 1
17 | pvObj.save()
18 |
19 | except Exception as ex:
20 | PageView.objects.create(
21 | url = request.path,
22 | view_count = 1
23 | )
24 |
25 | return ""
26 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templatetags/scoredisplayoptions.py:
--------------------------------------------------------------------------------
1 | from django import template
2 | from ..helpers import GOOGLE_SCORE_SCALE
3 |
4 | register = template.Library()
5 |
6 |
7 | ##
8 | ## Takes a score value and returns a category class name, used for coloring the circle.
9 | ## Score ranges and categories directly from Lighthouse documentation:
10 | ## https://developers.google.com/web/tools/lighthouse/v3/scoring
11 | ## Periodically check above URL for changes in ranges.
12 | ##
13 | @register.filter
14 | def scoreClass(scoreValue=0):
15 | """
16 | Returns class name based on the score performance.
17 | """
18 | try:
19 | if scoreValue <= GOOGLE_SCORE_SCALE['poor']['max']:
20 | returnClass = "pl-poorscore"
21 | elif scoreValue <= GOOGLE_SCORE_SCALE['average']['max']:
22 | returnClass = "pl-avgscore"
23 | elif scoreValue >= GOOGLE_SCORE_SCALE['good']['min']:
24 | returnClass = "pl-goodscore"
25 | else:
26 | returnClass = ""
27 | except Exception as ex:
28 | returnClass = ""
29 |
30 | return returnClass
31 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/templatetags/template_helpers.py:
--------------------------------------------------------------------------------
1 | from django import template
2 |
3 | register = template.Library()
4 |
5 | ##
6 | ## Global template HTML helpers for site consistency and easy redesigns.
7 | ##
8 | @register.simple_tag(takes_context=True)
9 | def getTemplateHelpers(context):
10 |
11 | siteColor = 'gold'
12 | horizontalSpace = 'ph3 ph4-ns'
13 | rounded = 'br2'
14 |
15 | commonButton = 'pointer mb3 ba ph4 pv3 bg-animate border-box ' + rounded
16 | smallButton = 'pointer mb3 ba pa2 bg-animate border-box ' + rounded
17 |
18 | bluePriButton = 'b--dark-blue bg-blue hover-bg-dark-blue white'
19 | blueSecButton = 'b--blue bg-white hover-bg-blue blue hover-white link'
20 |
21 | greenPriButton = 'b--dark-green bg-green hover-bg-dark-green white'
22 |
23 | icons = {
24 | 'chevronForward': ' ',
25 | 'newWindow': ' ',
26 | 'info': ' ',
27 | 'modal': ' '
28 | }
29 |
30 |
31 | return {
32 | 'classes': {
33 | 'button': commonButton,
34 | 'smallButton': smallButton,
35 | 'bluePriButton': bluePriButton,
36 | 'blueSecButton': blueSecButton,
37 | 'greenPriButton': greenPriButton,
38 | 'grid': horizontalSpace + ' w-100',
39 | 'horizontalSpace': horizontalSpace,
40 | 'hasIcon': 'inline-flex items-center underline-hover',
41 | 'imageBorder': 'ba b--black-20',
42 | 'navItem': 'link near-white f6 f5-ns fl relative mr4 pv3 hover-%s' % (siteColor),
43 | 'rounded': rounded,
44 | 'siteColor': siteColor,
45 | 'spinner': 'pl-spinner ba br-100',
46 | 'tableListCell': 'pv3 bb b--black-20',
47 | 'tableListCell_bt': 'pv3 bt b--black-20',
48 | 'tooltipCue': 'bb b--black-20 b--dashed pointer bt-0 br-0 bl-0',
49 | 'viewAll': commonButton + ' b--blue bg-white hover-bg-blue blue hover-white link',
50 | 'viewReport': commonButton + ' b--dark-green bg-green hover-bg-dark-green white',
51 | },
52 | 'html': {
53 | 'hr': '',
54 | 'icons': icons,
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # test
2 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/tests/test_filters.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from django.contrib.auth.models import User
4 |
5 | from ..models import *
6 |
7 | class TestUrlFilters(TestCase):
8 |
9 | def setUp(self):
10 | """
11 | create some urls and some filters
12 | """
13 | superuser = User(
14 | first_name='super',
15 | is_staff=True,
16 | is_superuser=True,
17 | last_name='User',
18 | username='superuser',
19 | )
20 | superuser.set_password('password!')
21 | superuser.save()
22 |
23 | urls = [
24 | 'https://ibm.com/foo',
25 | 'https://ibm.com/bar/baz/biff',
26 | 'https://ibm.com/bar/baz/#w00t',
27 | ]
28 |
29 | url_arr = []
30 |
31 | for url in urls:
32 | url_arr.append(
33 | Url.objects.create(
34 | created_by=superuser,
35 | edited_by=superuser,
36 | url=url
37 | )
38 | )
39 |
40 | # make 3 UrlFilters
41 | filters = ['foo', 'bar', 'w00t']
42 | filter_arr = []
43 |
44 | for filter in filters:
45 | filter_arr.append(
46 | UrlFilter.objects.create(
47 | name='%s filter' % filter,
48 | description='%s filter description' % filter
49 | )
50 | )
51 |
52 | for filter in filters:
53 | if filter == 'foo':
54 | UrlFilterPart.objects.create(
55 | prop='path_segment',
56 | filter_val=filter,
57 | url_filter=filter_arr[0]
58 | )
59 | elif filter == 'bar':
60 | UrlFilterPart.objects.create(
61 | prop='path_segment',
62 | filter_val='baz',
63 | filter_path_index=1,
64 | url_filter=filter_arr[1]
65 | )
66 | elif filter == 'w00t':
67 | UrlFilterPart.objects.create(
68 | prop='hash',
69 | filter_val='w00t',
70 | url_filter=filter_arr[2]
71 | )
72 |
73 | def test_UrlFilters(self):
74 | # import ipdb; ipdb.set_trace()
75 | foo = UrlFilter.objects.get(name='foo filter')
76 | # import ipdb; ipdb.set_trace()
77 | urls = foo.run_query()
78 |
79 | self.assertEqual(urls[0].url, 'https://ibm.com/foo')
80 |
81 | bar = UrlFilter.objects.get(name='bar filter')
82 | urls = bar.run_query()
83 |
84 | self.assertEqual(urls[0].url, 'https://ibm.com/bar/baz/biff')
85 |
86 | woot = UrlFilter.objects.get(name='w00t filter')
87 | urls = woot.run_query()
88 |
89 | self.assertEqual(urls[0].url, 'https://ibm.com/bar/baz/#w00t')
90 |
--------------------------------------------------------------------------------
/admin/pageaudit/report/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url, include
2 | from django.urls import path
3 | from django.contrib.auth.views import logout
4 | from django.views.generic import RedirectView, TemplateView
5 |
6 | from .views import *
7 |
8 | ## URLS are from context of /report/ so don't include that in here.
9 | ## NAMESPACE for these URLS is "plr" (page-lab report).
10 | ## So {% url 'plr:home' %} points to the "/report/" URL using "home" view
11 | urlpatterns = [
12 |
13 | ## APIs.
14 | url(r'^api/urlid/$', api_urlid, name='api_urlid'),
15 | url(r'^api/lighthousedata/((?P[\d-]+)/)?$', api_lighthouse_data, name='api_lighthouse_data'),
16 | url(r'^api/compareinfo/$', api_compareinfo, name='api_compareinfo'),
17 | url(r'^api/browse/items/$', api_browse_items, name='api_browse_items'),
18 | url(r'^api/urltypeahead/$', api_url_typeahead, name='api_url_typeahead'),
19 | url(r'^api/chart/scores/$', api_chart_scores, name='api_chart_scores'),
20 | url(r'^api/table/kpis/$', api_table_kpis, name='api_table_kpis'),
21 |
22 | ## Core pages.
23 | ## Regex on browse and dashboard allow capture of just the filter slug, excluding the /.
24 | ## Need this for URL reverses in templates.
25 | url(r'^$', home, name='home'),
26 | url(r'^browse/$', reports_browse, name='reports_browse'),
27 | url(r'^dashboard/$', reports_dashboard, name='reports_dashboard'),
28 | url(r'^filters/$', reports_filters, name='reports_filters'),
29 | url(r'^urls/detail/(?P[\d-]+)/$', reports_urls_detail, name='reports_urls_detail'),
30 |
31 | ## Compare page.
32 | ## First 2 IDs are required, 3rd is optional, more than 3 is wrong.
33 | ## I know this can be a regex to combine these but it's easier to read and maintain like this.
34 | url(r'^urls/compare/(?P[\d-]+)/(?P[\d-]+)/?$', reports_urls_compare, name='reports_urls_compare'),
35 | url(r'^urls/compare/(?P[\d-]+)/(?P[\d-]+)/(?P[\d-]+)/$', reports_urls_compare, name='reports_urls_compare'),
36 | url(r'^urls/compare/(?P[\d-]+)/(?P[\d-]+)/(?P[\d-]+)/(.*)', RedirectView.as_view(url=reverse_lazy('plr:home'))),
37 |
38 | ## Lighthouse report data viewer.
39 | url(r'^urls/lighthouse-viewer/(?P[\d-]+)/$', reports_lighthouse_viewer, name='reports_lighthouse_viewer'),
40 | url(r'^urls/lighthouse-viewer-template/$', TemplateView.as_view(template_name='reports_lighthouse_viewer_template.html'), name='reports_lighthouse_viewer_template'),
41 |
42 | ## Standard across all apps.
43 | url(r'^signin/$', signin, name='signin'),
44 | url(r'^signout/$', logout, name='signout'),
45 | url(r'^signedout/$', signedout, name='signedout'),
46 |
47 | ## All flat pages served out of this dir.
48 | path('pages/', include('django.contrib.flatpages.urls')),
49 |
50 | ## Here for dev/testing in debug mode:
51 | url(r'^404$', custom_404),
52 | url(r'^500$', custom_500),
53 | ]
54 |
--------------------------------------------------------------------------------
/admin/pageaudit/runserver:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 | ./manage.py runserver_plus --cert-file ./_dev/ssl-cert-snakeoil.pem
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/c3.min.css:
--------------------------------------------------------------------------------
1 | .c3 svg{font:10px sans-serif;-webkit-tap-highlight-color:transparent}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc rect{stroke:#fff;stroke-width:1}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:grey;font-size:2em}.c3-line{stroke-width:1px}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:1;fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-title{font:14px sans-serif}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #ccc}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#fff}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip td.value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max{fill:#777}.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}.c3-chart-arc.c3-target g path{opacity:1}.c3-chart-arc.c3-target.c3-focused g path{opacity:1}.c3-drag-zoom.enabled{pointer-events:all!important;visibility:visible}.c3-drag-zoom.disabled{pointer-events:none!important;visibility:hidden}.c3-drag-zoom .extent{fill-opacity:.1}
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/datatables/colReorder.dataTables.min.css:
--------------------------------------------------------------------------------
1 | table.DTCR_clonedTable.dataTable{position:absolute !important;background-color:rgba(255,255,255,0.7);z-index:202}div.DTCR_pointer{width:1px;background-color:#0259C4;z-index:201}
2 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/datatables/images/sort_asc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/css/datatables/images/sort_asc.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/datatables/images/sort_both.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/css/datatables/images/sort_both.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/datatables/images/sort_desc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/css/datatables/images/sort_desc.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/datatables/responsive.dataTables.min.css:
--------------------------------------------------------------------------------
1 | table.dataTable.dtr-inline.collapsed>tbody>tr>td.child,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty{cursor:default !important}table.dataTable.dtr-inline.collapsed>tbody>tr>td.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>th.child:before,table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty:before{display:none !important}table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>td:first-child,table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>th:first-child{position:relative;padding-left:30px;cursor:pointer}table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr[role="row"]>th:first-child:before{top:9px;left:4px;height:14px;width:14px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td:first-child:before,table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th:first-child:before{content:'-';background-color:#d33333}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child{padding-left:27px}table.dataTable.dtr-inline.collapsed.compact>tbody>tr>td:first-child:before,table.dataTable.dtr-inline.collapsed.compact>tbody>tr>th:first-child:before{top:5px;left:4px;height:14px;width:14px;border-radius:14px;line-height:14px;text-indent:3px}table.dataTable.dtr-column>tbody>tr>td.control,table.dataTable.dtr-column>tbody>tr>th.control{position:relative;cursor:pointer}table.dataTable.dtr-column>tbody>tr>td.control:before,table.dataTable.dtr-column>tbody>tr>th.control:before{top:50%;left:50%;height:16px;width:16px;margin-top:-10px;margin-left:-10px;display:block;position:absolute;color:white;border:2px solid white;border-radius:14px;box-shadow:0 0 3px #444;box-sizing:content-box;text-align:center;text-indent:0 !important;font-family:'Courier New', Courier, monospace;line-height:14px;content:'+';background-color:#31b131}table.dataTable.dtr-column>tbody>tr.parent td.control:before,table.dataTable.dtr-column>tbody>tr.parent th.control:before{content:'-';background-color:#d33333}table.dataTable>tbody>tr.child{padding:0.5em 1em}table.dataTable>tbody>tr.child:hover{background:transparent !important}table.dataTable>tbody>tr.child ul.dtr-details{display:inline-block;list-style-type:none;margin:0;padding:0}table.dataTable>tbody>tr.child ul.dtr-details>li{border-bottom:1px solid #efefef;padding:0.5em 0}table.dataTable>tbody>tr.child ul.dtr-details>li:first-child{padding-top:0}table.dataTable>tbody>tr.child ul.dtr-details>li:last-child{border-bottom:none}table.dataTable>tbody>tr.child span.dtr-title{display:inline-block;min-width:75px;font-weight:bold}div.dtr-modal{position:fixed;box-sizing:border-box;top:0;left:0;height:100%;width:100%;z-index:100;padding:10em 1em}div.dtr-modal div.dtr-modal-display{position:absolute;top:0;left:0;bottom:0;right:0;width:50%;height:50%;overflow:auto;margin:auto;z-index:102;overflow:auto;background-color:#f5f5f7;border:1px solid black;border-radius:0.5em;box-shadow:0 12px 30px rgba(0,0,0,0.6)}div.dtr-modal div.dtr-modal-content{position:relative;padding:1em}div.dtr-modal div.dtr-modal-close{position:absolute;top:6px;right:6px;width:22px;height:22px;border:1px solid #eaeaea;background-color:#f9f9f9;text-align:center;border-radius:3px;cursor:pointer;z-index:12}div.dtr-modal div.dtr-modal-close:hover{background-color:#eaeaea}div.dtr-modal div.dtr-modal-background{position:fixed;top:0;left:0;right:0;bottom:0;z-index:101;background:rgba(0,0,0,0.6)}@media screen and (max-width: 767px){div.dtr-modal div.dtr-modal-display{width:95%}}
2 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/hint.base.min.css:
--------------------------------------------------------------------------------
1 | /*! Hint.css (base version) - v2.5.1 - 2018-11-17
2 | * http://kushagragour.in/lab/hint/
3 | * Copyright (c) 2018 Kushagra Gour */
4 |
5 | [class*=hint--]{position:relative;display:inline-block}[class*=hint--]:after,[class*=hint--]:before{position:absolute;-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);transform:translate3d(0,0,0);visibility:hidden;opacity:0;z-index:1000000;pointer-events:none;-webkit-transition:.3s ease;-moz-transition:.3s ease;transition:.3s ease;-webkit-transition-delay:0s;-moz-transition-delay:0s;transition-delay:0s}[class*=hint--]:hover:after,[class*=hint--]:hover:before{visibility:visible;opacity:1;-webkit-transition-delay:.1s;-moz-transition-delay:.1s;transition-delay:.1s}[class*=hint--]:before{content:'';position:absolute;background:0 0;border:6px solid transparent;z-index:1000001}[class*=hint--]:after{background:#383838;color:#fff;padding:8px 10px;font-size:12px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;line-height:12px;white-space:nowrap}[class*=hint--][aria-label]:after{content:attr(aria-label)}[class*=hint--][data-hint]:after{content:attr(data-hint)}[aria-label='']:after,[aria-label='']:before,[data-hint='']:after,[data-hint='']:before{display:none!important}.hint--top-left:before,.hint--top-right:before,.hint--top:before{border-top-color:#383838}.hint--bottom-left:before,.hint--bottom-right:before,.hint--bottom:before{border-bottom-color:#383838}.hint--top:after,.hint--top:before{bottom:100%;left:50%}.hint--top:before{margin-bottom:-11px;left:calc(50% - 6px)}.hint--top:after{-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);transform:translateX(-50%)}.hint--top:hover:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--top:hover:after{-webkit-transform:translateX(-50%) translateY(-8px);-moz-transform:translateX(-50%) translateY(-8px);transform:translateX(-50%) translateY(-8px)}.hint--bottom:after,.hint--bottom:before{top:100%;left:50%}.hint--bottom:before{margin-top:-11px;left:calc(50% - 6px)}.hint--bottom:after{-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);transform:translateX(-50%)}.hint--bottom:hover:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--bottom:hover:after{-webkit-transform:translateX(-50%) translateY(8px);-moz-transform:translateX(-50%) translateY(8px);transform:translateX(-50%) translateY(8px)}.hint--right:before{border-right-color:#383838;margin-left:-11px;margin-bottom:-6px}.hint--right:after{margin-bottom:-14px}.hint--right:after,.hint--right:before{left:100%;bottom:50%}.hint--right:hover:after,.hint--right:hover:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)}.hint--left:before{border-left-color:#383838;margin-right:-11px;margin-bottom:-6px}.hint--left:after{margin-bottom:-14px}.hint--left:after,.hint--left:before{right:100%;bottom:50%}.hint--left:hover:after,.hint--left:hover:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--top-left:after,.hint--top-left:before{bottom:100%;left:50%}.hint--top-left:before{margin-bottom:-11px;left:calc(50% - 6px)}.hint--top-left:after{-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);transform:translateX(-100%);margin-left:12px}.hint--top-left:hover:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--top-left:hover:after{-webkit-transform:translateX(-100%) translateY(-8px);-moz-transform:translateX(-100%) translateY(-8px);transform:translateX(-100%) translateY(-8px)}.hint--top-right:after,.hint--top-right:before{bottom:100%;left:50%}.hint--top-right:before{margin-bottom:-11px;left:calc(50% - 6px)}.hint--top-right:after{-webkit-transform:translateX(0);-moz-transform:translateX(0);transform:translateX(0);margin-left:-12px}.hint--top-right:hover:after,.hint--top-right:hover:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--bottom-left:after,.hint--bottom-left:before{top:100%;left:50%}.hint--bottom-left:before{margin-top:-11px;left:calc(50% - 6px)}.hint--bottom-left:after{-webkit-transform:translateX(-100%);-moz-transform:translateX(-100%);transform:translateX(-100%);margin-left:12px}.hint--bottom-left:hover:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--bottom-left:hover:after{-webkit-transform:translateX(-100%) translateY(8px);-moz-transform:translateX(-100%) translateY(8px);transform:translateX(-100%) translateY(8px)}.hint--bottom-right:after,.hint--bottom-right:before{top:100%;left:50%}.hint--bottom-right:before{margin-top:-11px;left:calc(50% - 6px)}.hint--bottom-right:after{-webkit-transform:translateX(0);-moz-transform:translateX(0);transform:translateX(0);margin-left:-12px}.hint--bottom-right:hover:after,.hint--bottom-right:hover:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--large:after,.hint--medium:after,.hint--small:after{white-space:normal;line-height:1.4em;word-wrap:break-word}.hint--small:after{width:80px}.hint--medium:after{width:150px}.hint--large:after{width:300px}.hint--always:after,.hint--always:before{opacity:1;visibility:visible}.hint--always.hint--top:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--top:after{-webkit-transform:translateX(-50%) translateY(-8px);-moz-transform:translateX(-50%) translateY(-8px);transform:translateX(-50%) translateY(-8px)}.hint--always.hint--top-left:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--top-left:after{-webkit-transform:translateX(-100%) translateY(-8px);-moz-transform:translateX(-100%) translateY(-8px);transform:translateX(-100%) translateY(-8px)}.hint--always.hint--top-right:after,.hint--always.hint--top-right:before{-webkit-transform:translateY(-8px);-moz-transform:translateY(-8px);transform:translateY(-8px)}.hint--always.hint--bottom:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--bottom:after{-webkit-transform:translateX(-50%) translateY(8px);-moz-transform:translateX(-50%) translateY(8px);transform:translateX(-50%) translateY(8px)}.hint--always.hint--bottom-left:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--bottom-left:after{-webkit-transform:translateX(-100%) translateY(8px);-moz-transform:translateX(-100%) translateY(8px);transform:translateX(-100%) translateY(8px)}.hint--always.hint--bottom-right:after,.hint--always.hint--bottom-right:before{-webkit-transform:translateY(8px);-moz-transform:translateY(8px);transform:translateY(8px)}.hint--always.hint--left:after,.hint--always.hint--left:before{-webkit-transform:translateX(-8px);-moz-transform:translateX(-8px);transform:translateX(-8px)}.hint--always.hint--right:after,.hint--always.hint--right:before{-webkit-transform:translateX(8px);-moz-transform:translateX(8px);transform:translateX(8px)}
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/images/sort_asc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/css/images/sort_asc.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/images/sort_both.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/css/images/sort_both.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/images/sort_desc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/css/images/sort_desc.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/modal.css:
--------------------------------------------------------------------------------
1 | /************************************************************************************************
2 | Modal styles not achieved thru Tachyons classes.
3 | ************************************************************************************************/
4 | .pl-modal {
5 | display: none;
6 | }
7 |
8 | .pl-modal.is-open {
9 | display: block;
10 | }
11 |
12 | .pl-modal-close:before {
13 | content: "\2715";
14 | }
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/css/site-custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | Image via Wikimedia Commons:
3 | https://commons.wikimedia.org/wiki/File:Mad_scientist_transparent_background.svg
4 | Modified gloves by: Michael Santelia.
5 | **/
6 |
7 |
8 | /************************************************************************************************
9 | Site-wide style vars and base setups
10 | ************************************************************************************************/
11 | :root {
12 | --gold: #ffb700;
13 | --text-color: #333;
14 | --blue: #2571eb;
15 | --site-color: var(--gold);
16 | --animation-curve: cubic-bezier(0.2,1,0.2,1);
17 | }
18 |
19 | /**
20 | Make blue slightly darker to pass accessibility on buttons and links.
21 | Any other Tachyons "blue" classes used have to be (re)set here to the new blue.
22 | **/
23 | .blue {
24 | color: var(--blue);
25 | }
26 |
27 | .bg-blue,
28 | .hover-bg-blue:hover {
29 | background-color: var(--blue);
30 | }
31 |
32 | .b--blue {
33 | border-color: var(--blue);
34 | }
35 |
36 |
37 | html, body, button, input, select, textarea,
38 | .pl-textcolor-body {
39 | color: var(--text-color);
40 | }
41 |
42 | .icon {
43 | fill: var(--blue);
44 | width: 32px;
45 | }
46 |
47 | .icon.new-window,
48 | .icon.info {
49 | width: 24px;
50 | }
51 |
52 | a {
53 | color: var(--blue);
54 | text-decoration: none;
55 | }
56 |
57 | a .icon {
58 | transition: transform .4s var(--animation-curve);
59 | }
60 | a:hover .icon {
61 | transform: translate3d(4px,0,0);
62 | }
63 |
64 |
65 | /************************************************************************************************
66 | Site navigation bar
67 | ************************************************************************************************/
68 | .pl-sitenav .pl-highlight:after {
69 | background: var(--site-color);
70 | content: "";
71 | height: 4px;
72 | width: 100%;
73 | display: block;
74 | position: absolute;
75 | bottom: 0;
76 | }
77 |
78 |
79 |
80 | /************************************************************************************************
81 | Spinner
82 | ************************************************************************************************/
83 | .pl-spinner {
84 | animation: 0.6s linear 0s normal none infinite pl-spinner-kf-spin, 5s ease-in-out 0s normal none infinite pl-spinner-kf-colors;
85 | -webkit-transform: rotate(0deg);
86 | transform: rotate(0deg);
87 | }
88 |
89 | @-webkit-keyframes pl-spinner-kf-colors {
90 | 0% {
91 | border-color: rgba(0, 0, 0, 0) #ccc;
92 | }
93 |
94 | 50% {
95 | border-color: rgba(0, 0, 0, 0) var(--gold) ;
96 | }
97 |
98 | 100% {
99 | border-color: rgba(0, 0, 0, 0) #ccc;
100 | }
101 | }
102 | @keyframes pl-spinner-kf-colors {
103 | 0% {
104 | border-color: rgba(0, 0, 0, 0) var(--blue);
105 | }
106 |
107 | 50% {
108 | border-color: rgba(0, 0, 0, 0) var(--blue) var(--blue) ;
109 | }
110 |
111 | 100% {
112 | border-color: rgba(0, 0, 0, 0) var(--blue);
113 | }
114 | }
115 |
116 | @-webkit-keyframes pl-spinner-kf-spin {
117 | 0% {
118 | -webkit-transform: rotate(0deg);
119 | }
120 |
121 | 100% {
122 | -webkit-transform: rotate(-360deg);
123 | }
124 | }
125 |
126 | @keyframes pl-spinner-kf-spin {
127 | 0% {
128 | transform: rotate(0deg);
129 | }
130 |
131 | 100% {
132 | transform: rotate(-360deg);
133 | }
134 | }
135 |
136 |
137 | /************************************************************************************************
138 | Leadspace
139 | ************************************************************************************************/
140 | .pl-leadspace {
141 | background: #000 url(../img/leadspace.svg) no-repeat 95% 100% / 11%;
142 | min-height: 160px;
143 | }
144 |
145 |
146 |
147 | /************************************************************************************************
148 | Audit score donuts.
149 | ************************************************************************************************/
150 | .pl-audit-score {
151 | border: transparent 5px solid;
152 | line-height: 35px;
153 | padding: 10px;
154 | }
155 |
156 | .pl-goodscore {
157 | border-color: #008000;
158 | }
159 |
160 | .pl-avgscore {
161 | border-color: #f4a000;
162 | }
163 |
164 | .pl-poorscore {
165 | border-color: #e71d32;
166 | }
167 |
168 |
169 |
170 | /************************************************************************************************
171 | Typeahead drop-down menu list.
172 | This class is intended to go on the .
173 | ************************************************************************************************/
174 | @media only screen and (min-width: 0px) and (max-width: 579px) {
175 | .pl-dropdown-menu li {
176 | white-space: normal;
177 | }
178 | }
179 |
180 | .pl-dropdown-menu li::before {
181 | content: none;
182 | }
183 |
184 | .pl-dropdown-menu a {
185 | color: var(--text-color);
186 | padding: 7px 10px;
187 | }
188 |
189 | .pl-dropdown-menu .pl-highlight a,
190 | .pl-dropdown-menu a:hover,
191 | .pl-dropdown-menu a:focus {
192 | background-color: var(--blue);
193 | color: #fff;
194 | }
195 |
196 |
197 |
198 | /************************************************************************************************
199 | Compare tray
200 | ************************************************************************************************/
201 |
202 | #pl-compare {
203 | display: none;
204 | transform: translate3d(0, calc(100% - 50px),0);
205 | transition: transform .5s var(--animation-curve);
206 | }
207 |
208 | @media print {
209 | #pl-compare {
210 | display: none;
211 | }
212 | }
213 |
214 | /* Default; tray is hidden. When JS adds class, show tray and add footer padding. */
215 | .pl-compare-enabled #pl-compare {
216 | display: block;
217 | }
218 |
219 | .pl-compare-enabled {
220 | padding-bottom: 48px;
221 | }
222 |
223 | /* Slide tray up when 'open' link is clicked (JS adds 'opened' class.) */
224 | #pl-compare.opened {
225 | transform: translate3d(0,0,0);
226 | }
227 |
228 |
229 | #pl-compare .pl-compare-closetray {
230 | display: none;
231 | }
232 | #pl-compare.opened .pl-compare-closetray {
233 | display: block;
234 | }
235 |
236 |
237 | #pl-compare .pl-compare-opentray {
238 | display: block;
239 | }
240 | #pl-compare.opened .pl-compare-opentray {
241 | display: none;
242 | }
243 |
244 |
245 | /* Default: 'compare' link hidden, plain text shown. */
246 | .pl-compare-comparelink-con .pl-compare-plaintext {
247 | cursor: not-allowed;
248 | display: inline;
249 | }
250 | .pl-compare-comparelink-con .pl-compare-hotlink {
251 | display: none;
252 | }
253 |
254 | /* >1 items; 'compare' link shown, text hidden. */
255 | .pl-compare-comparelink-con.enabled .pl-compare-plaintext {
256 | display: none;
257 | }
258 | .pl-compare-comparelink-con.enabled .pl-compare-hotlink {
259 | display: inline;
260 | }
261 |
262 |
263 |
264 | /************************************************************************************************
265 | Reports browse/list page
266 | ************************************************************************************************/
267 |
268 | /* Add slight delay so fly-bys aren't so annoying. */
269 | .pl-card-con {
270 | transition: border .4s .15s;
271 | width: 330px;
272 | }
273 |
274 | .pl-reportcard-notests {
275 | background: rgba(0,0,0,0.75);
276 | left: 50%;
277 | top: 40%;
278 | transform: translate3d(-50%,-50%,0);
279 | width: 120px;
280 | }
281 |
282 | /* Add slight delay so fly-bys aren't so annoying. */
283 | .pl-compare-cbcon input,
284 | .pl-compare-cbcon label {
285 | transition: opacity .4s .15s;
286 | opacity: 0;
287 | }
288 |
289 | .pl-card-con:hover .pl-compare-cbcon *,
290 | .pl-compare-cbcon input:checked,
291 | .pl-compare-cbcon input:checked + label {
292 | opacity: 1;
293 | }
294 |
295 | .pl-compare-cbcon:hover * {
296 | color: var(--blue);
297 | }
298 |
299 |
300 |
301 | /************************************************************************************************
302 | Effects
303 | ************************************************************************************************/
304 | .pl-fadein {
305 | visibility: visible;
306 | opacity: 1;
307 | transition: opacity .4s;
308 | }
309 |
310 | .pl-fadeout {
311 | visibility: hidden;
312 | opacity: 0;
313 | transition: visibility 0s 0.4s, opacity 0.4s;
314 | }
315 |
316 | /** Makes a border color match the text color, on hover. **/
317 | .hover-b--current:hover,
318 | .hover-b--current:focus {
319 | border-color: currentColor;
320 | }
321 |
322 | .hover-b--blue:hover,
323 | .hover-b--blue:focus {
324 | border-color: var(--blue);
325 | }
326 |
327 |
328 |
329 | /************************************************************************************************
330 | Select 2 updates to match site design
331 | I know !important is bad but way less CSS here by using it.
332 | ************************************************************************************************/
333 | .select2-results__option--highlighted {
334 | background-color: var(--blue) !important;
335 | }
336 | .select2-container .select2-selection--single {
337 | height: 37px !important;
338 | }
339 | .select2-selection__rendered {
340 | line-height: 37px !important;
341 | padding-right: 22px !important;
342 | }
343 | .select2-selection__arrow {
344 | right: 3px !important;
345 | top: 7px !important;
346 | }
347 |
348 |
349 | /************************************************************************************************
350 | Datatable style overrides.
351 | ************************************************************************************************/
352 | .dataTable thead th {
353 | text-align: left;
354 | vertical-align: top;
355 | }
356 |
357 | .dataTable.hover tbody tr:hover,
358 | .dataTable.display tbody tr:hover,
359 | .dataTable.display tbody tr:hover > .sorting_1,
360 | .dataTable.order-column.hover tbody tr:hover > .sorting_1 {
361 | background-color: #cdecff !important;
362 | }
363 |
364 | .dataTable thead .sorting,
365 | .dataTable thead .sorting_asc,
366 | .dataTable thead .sorting_desc {
367 | background-position: -1px 11px !important;
368 | }
369 |
370 |
371 | .dataTable thead .sorting_asc {
372 | background-position: -1px 13px !important;
373 | }
374 |
375 | .dataTable thead .no-sort {
376 | background: none !important;
377 | }
378 |
379 | .dataTables_length {
380 | margin-bottom: 2rem;
381 | }
382 |
383 | .dtr-inline.collapsed > tbody td:first-child::before,
384 | .dtr-inline.collapsed > tbody th:first-child::before {
385 | top: unset !important;
386 | }
387 |
388 |
389 |
390 | /************************************************************************************************
391 | Tooltip styling
392 | ************************************************************************************************/
393 | [class*="hint--"]::after {
394 | border-radius: 4px;
395 | font-size: .875rem !important;
396 | }
397 |
398 |
399 |
400 | /************************************************************************************************
401 | Helpers
402 | ************************************************************************************************/
403 | .pl-resize {
404 | height: auto !important;
405 | width: 100%;
406 | }
407 |
408 | .pl-downsize {
409 | height: auto !important;
410 | max-width: 100% !important;
411 | }
412 |
413 | .pl-word-break-all {
414 | word-break: break-all;
415 | }
416 |
417 |
418 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/img/chevron-forward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/img/django.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
9 |
12 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/img/nodejs.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/img/notests.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/img/notests.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/img/postgresql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/img/postgresql.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/img/redis.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/js/compare.js:
--------------------------------------------------------------------------------
1 | (function ($, PL) {
2 |
3 | var compare = PL.namespace(PL, "compare");
4 |
5 | var $compareTray,
6 | $compareTrayBody,
7 | $compareTrayCompareLink,
8 | maxCompareNum = 3,
9 | compareLs = {
10 | keyName: "pagelabCompare",
11 | get: function () {
12 | return PL.util.storage.getItem(this.keyName);
13 | },
14 | set: function (arr) {
15 | return PL.util.storage.setItem(this.keyName, arr);
16 | },
17 | add: function (id) {
18 | var id = parseInt(id,10),
19 | arrIds = this.get() || [] ;
20 |
21 | // Prevent duplicates: If the item is already in the array, don't add it again.
22 | if (arrIds.indexOf(id) > -1) {
23 | return arrIds;
24 | }
25 | else {
26 | arrIds.push(id);
27 | return this.set(arrIds);
28 | }
29 | },
30 | remove: function (id) {
31 | var arrIds = this.get() || [] ;
32 | itemIndex = arrIds.indexOf(parseInt(id,10));
33 |
34 | if (itemIndex > -1) {
35 | arrIds.splice(itemIndex, 1);
36 | }
37 | return this.set(arrIds);
38 | }
39 | };
40 |
41 | // Set the above apis to be public callable via our PL namespace.
42 | compare.storage = compareLs;
43 |
44 |
45 | /**
46 | Called whenever we add/remove an item, this shows or hides the tray.
47 | If there are no items to compare, tray is hidden because no need for it.
48 | If there are any items, compare tray is shown.
49 |
50 | @method activateTray
51 | @private
52 | **/
53 | function activateTray () {
54 | if (getNumItemsInTray() > 0) {
55 | document.body.classList.add("pl-compare-enabled");
56 | }
57 | else {
58 | document.body.classList.remove("pl-compare-enabled");
59 | $compareTray.removeClass("opened");
60 | }
61 | }
62 |
63 |
64 | /**
65 | Callback from "getItemHtml".
66 | Does the actual injection of the item's HTML if it was returned properly.
67 |
68 | @method addToCompareTray
69 | @private
70 | @param data {Object/JSON} The data returned from the WSR (web service request).
71 | **/
72 | function addToCompareTray (data) {
73 | if (data && data.results && data.results.resultsHtml) {
74 | $compareTrayBody.append(data.results.resultsHtml);
75 | compareLs.add(data.results.id);
76 | }
77 | else {
78 | console.warn("Sorry, there was an error retrieving URL data for the compare tray.");
79 | }
80 |
81 | // Decide if we need to show/hide the tray and enable the 'compare' link.
82 | activateTray();
83 | enableCompareLink();
84 | }
85 |
86 |
87 | /**
88 | Evaluates and decides if the compare link should be active or not.
89 | If there is only 1 item in the tray, there's nothing to compare, so link is inactive.
90 | If there are >1 items in the tray, enables 'compare' link.
91 |
92 | @method enableCompareLink
93 | @private
94 | **/
95 | function enableCompareLink () {
96 | if (getNumItemsInTray() > 1) {
97 | $compareTrayCompareLink.addClass("enabled");
98 | }
99 | else {
100 | $compareTrayCompareLink.removeClass("enabled");
101 | }
102 | }
103 |
104 |
105 | /**
106 | Takes the URL's id and retrieves the HTML snippet to use and add to the compare tray.
107 |
108 | @method getItemHtml
109 | @private
110 | @param id {Integer} The ID of a URL object.
111 | **/
112 | function getItemHtml (id) {
113 | var xhr = new XMLHttpRequest();
114 | xhr.open('GET', PL.urls.api_compareinfo + "?id=" + id);
115 | xhr.onload = function() {
116 | if (xhr.status === 200) {
117 | var data = JSON.parse(xhr.responseText);
118 | addToCompareTray(data);
119 | }
120 | else {
121 | console.warn("Sorry, there was an error receiving the URL's info for the compare tray.");
122 | }
123 | };
124 | xhr.send();
125 | }
126 |
127 |
128 | /**
129 | Helper util function that tells us # of items in the tray, used for decisions
130 | on whether to show the tray and 'compare' link or not, etc.
131 | We don't just check "compareLs.get().length" in case user has cookies off,
132 | (even though no-cookies prevents cross-page state, at least on-page will work.)
133 |
134 | @method getNumItemsInTray
135 | @private
136 | @return {Integer} The # of items currently in the tray.
137 | **/
138 | function getNumItemsInTray () {
139 | return $(".pl-compare-item").length;
140 | }
141 |
142 |
143 | /**
144 | Pre-check a URL's checkbox if it is in the localStorage list of items to compare.
145 | This is called by each page that wants to have checkboxes setup to enable the user
146 | to select a UR to add to the compare tray.
147 |
148 | @method populateCompareTrayFromStorage
149 | @param $checkboxes {DOM elements/jQuery obj} A list of checkbox elements to test if one needs to be pre-checked.
150 | **/
151 | compare.preselectCheckbox = preselectCheckbox;
152 | function preselectCheckbox ($checkboxes) {
153 | $checkboxes.each(function () {
154 | var checkbox = this;
155 |
156 | if (compareLs.get() && compareLs.get().indexOf(parseInt(checkbox.value,10)) > -1) {
157 | $(checkbox).prop("checked", true);
158 | }
159 | });
160 | }
161 |
162 |
163 | /**
164 | Gets localStorage value (array IDs) and gets HTML for each and adds them to compare tray onload.
165 | Basically, this saves compare tray items across page loads, filters, sorts,
166 | etc without losing items they want to compare.
167 |
168 | @method populateCompareTrayFromStorage
169 | @private
170 | **/
171 | function populateCompareTrayFromStorage () {
172 | var existingIds = compareLs.get();
173 |
174 | // For each ID in the array in LS, get the HTML and populate the compare tray.
175 | $.each(existingIds, function () {
176 | var id = this;
177 | getItemHtml(id);
178 | });
179 | }
180 |
181 |
182 | /**
183 | Removes a URL from the compare tray and unchecks the box (if it's currently on the page).
184 |
185 | @method removeFromCompareTray
186 | @private
187 | @param id {Integer} The ID of a URL object.
188 | **/
189 | function removeFromCompareTray (id) {
190 | $compareTray.find("[data-itemid='" + id + "']").remove();
191 |
192 | $("#id_" + id).prop("checked", false);
193 |
194 | compareLs.remove(id);
195 |
196 | activateTray();
197 | enableCompareLink();
198 | }
199 |
200 |
201 | /**
202 | Binds (via defer/bubbling) all checkboxes inside the passed element to enable them
203 | to add/remove items in the compare tray.
204 | This is called by each page that wants to have checkboxes setup to enable the user
205 | to select a UR to add to the compare tray.
206 |
207 | @method setupCompareCheckboxes
208 | @param $elContainer {DOM/jQuery object} DOM element to search thru and bind checkboxes
209 | to be able to add/remove items to the compare tray.
210 | **/
211 | compare.setupCompareCheckboxes = setupCompareCheckboxes;
212 | function setupCompareCheckboxes ($elContainer) {
213 | $elContainer.on("change", "input", function (evt) {
214 | // If the box is checked, and there's an available slot, add the URL.
215 | if (this.checked === true) {
216 | if (getNumItemsInTray() === maxCompareNum) {
217 | alert("You can only compare up to three URLs.");
218 | $(this).prop("checked", false);
219 | return;
220 | }
221 | else {
222 | getItemHtml(this.value);
223 | }
224 | }
225 | // Otherwise they unchecked it so remove the item.
226 | else {
227 | removeFromCompareTray(this.value);
228 | }
229 | });
230 | }
231 |
232 |
233 | /**
234 | Sets up ALL actions on the compare tray: Open/close, Remove item, Clear all, Compare link.
235 | Rather simply have them all in 1 defer/bubble binding than individual el bindings.
236 |
237 | @method setupCompareTrayActions
238 | @private
239 | **/
240 | function setupCompareTrayActions () {
241 | $compareTray.on("click", "a", function (evt) {
242 | // ALL TRAY LINKS handled here, so we kill default onclick no matter what.
243 | evt.preventDefault();
244 |
245 | // Open/close link (+/- on left.)
246 | if (this.className.indexOf("pl-compare-closetray") > -1 || this.className.indexOf("pl-compare-opentray") > -1) {
247 | $compareTray.toggleClass("opened");
248 | }
249 | // "Compare" link (in center.)
250 | else if (this.className.indexOf("pl-compare-comparelink") > -1) {
251 | var urlIds = "";
252 |
253 | if (getNumItemsInTray() < 2) {
254 | return;
255 | }
256 |
257 | $(".pl-compare-item").each(function () {
258 | urlIds += $(this).data("itemid") + "/";
259 | });
260 |
261 | document.location.href = PL.urls.home + "urls/compare/" + urlIds;
262 | }
263 | // "Clear all" link (on right side.)
264 | else if (this.className.indexOf("pl-compare-clearall") > -1) {
265 | $(".pl-compare-item").each(function () {
266 | removeFromCompareTray($(this).data("itemid"));
267 | });
268 | }
269 | // "Remove" link (above each item).
270 | else if (this.className.indexOf("pl-compare-item-remove") > -1) {
271 | removeFromCompareTray($(this).data("itemid"));
272 | }
273 | });
274 | }
275 |
276 |
277 | // Showtime.
278 |
279 | // Sanity check. If there's more than 3 items in LS, it's tainted, so kill it.
280 | if (compareLs.get() && compareLs.get().length > 3) {
281 | compareLs.set([]);
282 | }
283 |
284 | // Strictly sets up the tray.
285 | // Checkbox bindings are util fuction each page calls as appropriate onload.
286 | $(function () {
287 | if (document.getElementById("pl-compare")) {
288 | $compareTray = $("#pl-compare");
289 | $compareTrayBody = $("#pl-compare-body");
290 | $compareTrayCompareLink = $(".pl-compare-comparelink-con")
291 | setupCompareTrayActions();
292 | populateCompareTrayFromStorage();
293 | activateTray();
294 | }
295 | });
296 |
297 |
298 | })(jQuery, PL);
299 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/js/micromodal.min.js:
--------------------------------------------------------------------------------
1 | !function(e,o){"object"==typeof exports&&"undefined"!=typeof module?module.exports=o():"function"==typeof define&&define.amd?define(o):e.MicroModal=o()}(this,function(){"use strict"
2 | var e=function(e,o){if(!(e instanceof o))throw new TypeError("Cannot call a class as a function")},o=function(){function e(e,o){for(var t=0;t0&&this.registerTriggers.apply(this,t(r)),this.onClick=this.onClick.bind(this),this.onKeydown=this.onKeydown.bind(this)}return o(n,[{key:"registerTriggers",value:function(){for(var e=this,o=arguments.length,t=Array(o),i=0;i'),!1},l=function(e){if(e.length<=0)return console.warn("MicroModal v0.3.1: ❗Please specify at least one %c'micromodal-trigger'","background-color: #f8f9fa;color: #50596c;font-weight: bold;","data attribute."),console.warn("%cExample:","background-color: #f8f9fa;color: #50596c;font-weight: bold;",' '),!1},c=function(e,o){if(l(e),!o)return!0
18 | for(var t in o)s(t)
19 | return!0}
20 | return{init:function(e){var o=Object.assign({},{openTrigger:"data-micromodal-trigger"},e),i=[].concat(t(document.querySelectorAll("["+o.openTrigger+"]"))),a=r(i,o.openTrigger)
21 | if(!0!==o.debugMode||!1!==c(i,a))for(var s in a){var l=a[s]
22 | o.targetModal=s,o.triggers=[].concat(t(l)),new n(o)}},show:function(e,o){var t=o||{}
23 | t.targetModal=e,!0===t.debugMode&&!1===s(e)||(a=new n(t),a.showModal())},close:function(){a.closeModal()}}}()})
24 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/js/site-base.js:
--------------------------------------------------------------------------------
1 | (function ($, PL) {
2 |
3 | // Core namespace utility
4 | PL.namespace = function() {
5 | var scope = arguments[0],
6 | ln = arguments.length,
7 | i, value, split, x, xln, parts, object;
8 |
9 | for (i = 1; i < ln; i++) {
10 | value = arguments[i];
11 | parts = value.split(".");
12 | object = scope[parts[0]] = Object(scope[parts[0]]);
13 | for (x = 1, xln = parts.length; x < xln; x++) {
14 | object = object[parts[x]] = Object(object[parts[x]]);
15 | }
16 | }
17 | return object;
18 | };
19 |
20 |
21 | })(jQuery, PL);
22 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/js/url-typeahead.js:
--------------------------------------------------------------------------------
1 |
2 | (function($) {
3 | var $inputField = {},
4 | $searchForm = {},
5 | $typeaheadUl = {},
6 | $typeaheadContainer = {},
7 | defaultTypeaheadRequestPause = 50, // # of MS to wait between typing before making a WSR.
8 | latestText = "",
9 | requestCount = 0,
10 | typeaheadResultsShowing = false,
11 | makeTypeaheadRequest = (function() {
12 | var timer = 0;
13 | return function(callback, ms) {
14 | var waitTime = ms || defaultTypeaheadRequestPause;
15 | clearTimeout(timer);
16 | timer = setTimeout(callback, waitTime);
17 | };
18 | })();
19 |
20 |
21 | function clearTypeahead () {
22 | if ($typeaheadUl.length > 0) {
23 | $typeaheadUl.empty();
24 | }
25 | }
26 |
27 |
28 | function requestTypeaheadText (forceRequest) {
29 | var currentSearchTerm = $inputField[0].value;
30 |
31 | if (currentSearchTerm === latestText && !forceRequest) {
32 | return;
33 | }
34 |
35 | latestText = currentSearchTerm;
36 |
37 | // If they cleared the search field (there's no value in the text field), remove the typeahead box and stop.
38 | if (currentSearchTerm === "") {
39 | makeTypeaheadRequest(function() {
40 | clearTypeahead();
41 | showTypeaheadResults(false);
42 | $searchForm.attr("action", "");
43 | }, defaultTypeaheadRequestPause + 10);
44 |
45 | return;
46 | }
47 |
48 | // Call the throttler with our callback function to run to make a typeahead service request.
49 | makeTypeaheadRequest(function() {
50 | $.ajax({
51 | url: PL.urls.api_url_typeahead + "?q=" + currentSearchTerm,
52 | dataType: "json",
53 | searchTerm: currentSearchTerm,
54 | requestCount: ++requestCount,
55 | success: function (response) {
56 | // If this request isn't the latest one (for slow API responses),
57 | // don't update the TA box with it's results.
58 | if (requestCount !== this.requestCount) {
59 | return;
60 | }
61 |
62 | if (response === null) {
63 | showTypeaheadResults(false);
64 | return;
65 | }
66 |
67 | var listArr = [],
68 | i = 0,
69 | len = 1
70 | results = response.results;
71 |
72 | if (results.length > 0) {
73 | len = results.length;
74 |
75 | // Loop thru all results. The createContainer function determines the limit to show from this array.
76 | for (i; i < len; i++) {
77 | if (results[i].url) {
78 | listArr.push(results[i]);
79 | }
80 | }
81 | }
82 | else {
83 | listArr.push({
84 | id: 0,
85 | url: "No results found"
86 | });
87 | }
88 |
89 | // Call the masthead typeahead API with the term used for *this WSR*
90 | // and the array you created of results to show.
91 | createTypeaheadContainer(this.searchTerm, listArr);
92 | },
93 | error: function(response) {
94 | console.error('Error calling typeahead service: ', response);
95 | }
96 | });
97 | });
98 | }
99 |
100 | // This limits the # that we show in the container.
101 | function createTypeaheadContainer (searchString, results) {
102 | var items = results,
103 | lis = "",
104 | maxNum = 6,
105 | searchedFor = searchString;
106 |
107 | items.sort();
108 |
109 | // Update this to better include min/max #s with 'for'.
110 | $.each(items, function (i, itemObj) {
111 | var term = itemObj.url,
112 | reg = new RegExp(searchedFor, 'i');
113 |
114 | term = term.replace(reg, '' + searchedFor + ' ');
115 |
116 | if (i < maxNum) {
117 | lis += '' + term + ' ';
118 | }
119 | });
120 |
121 | // If they emptied the field after this WSR ran, clear the typeaheads.
122 | if ($inputField.val() === "") {
123 | clearTypeahead();
124 | showTypeaheadResults(false);
125 | }
126 | else {
127 | // Inject typeahead list on first time we have results.
128 | if (!$typeaheadContainer.find("ul")[0]) {
129 | $typeaheadContainer.html($typeaheadUl);
130 | }
131 |
132 | $typeaheadUl.html(lis);
133 |
134 | showTypeaheadResults(true);
135 | }
136 | }
137 |
138 | function showTypeaheadResults (b) {
139 | if (b) {
140 | $typeaheadContainer.addClass("pl-fadein").removeClass("pl-fadeout");
141 | typeaheadResultsShowing = true;
142 | }
143 | else {
144 | $typeaheadContainer.addClass("pl-fadeout").removeClass("pl-fadein");
145 | typeaheadResultsShowing = false;
146 | }
147 | }
148 |
149 |
150 | // Setup and bind fields
151 | function setupFields () {
152 | $inputField = $("#pl-url-search");
153 | $searchForm = $inputField.closest("form");
154 | $typeaheadContainer = $("#pl-typeahead-container");
155 | $typeaheadUl = $('');
156 |
157 | $searchForm.on("submit", function (evt) {
158 | evt.preventDefault();
159 |
160 | // Hit service to validate URL report exists (in case of free-type in URL)
161 | // Returns the ID or null.
162 | // If not null, goto ID.
163 | var requestUrl = PL.urls.api_urlid + "?url=" + $inputField[0].value
164 | var xhr = new XMLHttpRequest();
165 | xhr.open('GET', requestUrl);
166 | xhr.onload = function() {
167 | if (xhr.status === 200) {
168 | var data = JSON.parse(xhr.responseText);
169 | try {
170 | if (data.results.urlid) {
171 | window.location.href = "urls/detail/" + data.results.urlid;
172 | }
173 | else {
174 | showTypeaheadResults(true);
175 | }
176 | }
177 | catch (ex) {
178 | showTypeaheadResults(true);
179 | }
180 | }
181 | else {
182 | alert("DOH! Something happened and we couldn't find that URL.");
183 | }
184 | };
185 | xhr.send();
186 | })
187 |
188 | // Bind the results so when you click on one, it replaces the input text with it.
189 | $typeaheadUl.on("click", function (evt) {
190 | evt.preventDefault();
191 | evt.stopPropagation();
192 |
193 | if ($(evt.target).parent().data("urlid") !== 0) {
194 | $inputField.val(evt.target.text);
195 | $searchForm.trigger("submit");
196 | }
197 | });
198 |
199 | $inputField.on("input", function () {
200 | requestTypeaheadText();
201 | }).on("focus", function () {
202 | // If there is a value in the field, show the results on FIELD focus only.
203 | if ($inputField.val() !== "") {
204 | showTypeaheadResults(true);
205 | }
206 | }).on("keydown", function (evt) {
207 | // HAVE to use keycode for xbrowser support.
208 | var keyCode = evt.keyCode;
209 |
210 | // LEFT/RIGHT/TAB
211 | // Let natural behavior happen if: left/right arrow or tab.
212 | if (keyCode === 37 || keyCode === 39) {
213 | return;
214 | }
215 |
216 | if (evt.keyCode === 9) {
217 | showTypeaheadResults(false);
218 | }
219 |
220 | // UP/DOWN:
221 | // If not showing, request current value typeahead
222 | // else goto next if it's already showing.
223 | // UP
224 | if (keyCode === 38) {
225 | evt.preventDefault();
226 |
227 | if (!typeaheadResultsShowing) {
228 | requestTypeaheadText();
229 | }
230 | else {
231 | gotoPrevTypeaheadResult();
232 | setInputFieldValue();
233 | }
234 | }
235 | // DOWN
236 | else if (keyCode === 40) {
237 | evt.preventDefault();
238 |
239 | if (!typeaheadResultsShowing) {
240 | requestTypeaheadText();
241 | }
242 | else {
243 | gotoNextTypeaheadResult();
244 | setInputFieldValue();
245 | }
246 | }
247 | }).on("blur", function (evt) {
248 | showTypeaheadResults(false);
249 | });
250 | }
251 |
252 |
253 | function gotoNextTypeaheadResult () {
254 | var $nextItem = $typeaheadUl.find("li.pl-highlight").next("li");
255 |
256 | $typeaheadUl.find("li.pl-highlight").removeClass("pl-highlight");
257 |
258 | if ($nextItem[0]) {
259 | $nextItem.addClass("pl-highlight");
260 | }
261 | else {
262 | $("li:first", $typeaheadUl).addClass("pl-highlight");
263 | }
264 |
265 | // If this item if the section heading, skip it and goto the next one.
266 | if ($typeaheadUl.find("li.pl-highlight").hasClass("typeahead-nooption")) {
267 | gotoNextTypeaheadResult();
268 | }
269 | }
270 |
271 |
272 | function gotoPrevTypeaheadResult () {
273 | var $prevItem = $typeaheadUl.find("li.pl-highlight").prev("li");
274 |
275 | $typeaheadUl.find("li.pl-highlight").removeClass("pl-highlight");
276 |
277 | if ($prevItem[0]) {
278 | $prevItem.addClass("pl-highlight");
279 | }
280 | else {
281 | $("li:last", $typeaheadUl).addClass("pl-highlight");
282 | }
283 |
284 | // If this item if the section heading, skip it and goto the next one.
285 | if ($typeaheadUl.find("li.pl-highlight").hasClass("typeahead-nooption")) {
286 | gotoPrevTypeaheadResult();
287 | }
288 | }
289 |
290 |
291 | function setInputFieldValue () {
292 | // Set active dedcendent attr. on field to tell ATs this one is active so they read it since we don't
293 | // focus on it, then change the text in the text field.
294 | var $highlightedLi = $typeaheadUl.find("li.pl-highlight");
295 |
296 | $inputField.attr("aria-activedescendant", $highlightedLi.attr("id"));
297 |
298 | $inputField.val($highlightedLi.text());
299 | }
300 |
301 |
302 | // Onload, setup bindings and fields.
303 | $(setupFields);
304 |
305 | })(jQuery);
306 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/js/util-storage.js:
--------------------------------------------------------------------------------
1 | (function ($, PL) {
2 |
3 | var storageUtil = PL.namespace(PL, "util.storage");
4 |
5 |
6 | /**
7 | Clears the user's browser localStorage (for the owning domain).
8 | CAUTION: This is the localStorage equivalent of clearing all of the user's cookies for your domain.
9 |
10 | @method clear
11 | @return {Boolean} True if localStorage is supported, else false.
12 | **/
13 | storageUtil.clear = function () {
14 | if (!storageUtil.isSupported()) {
15 | return false;
16 | }
17 |
18 | localStorage.clear();
19 | return true;
20 | };
21 |
22 | /**
23 | Gets the requested item from browser localStorage.
24 |
25 | @method getItem
26 | @param key {String} The name of the key/item to get from localStorage.
27 | @return {Varies} The data for the key if localStorage is supported && if key exists && key is not expired,
28 | else returns: null.
29 | **/
30 | storageUtil.getItem = function (key) {
31 | var storageData = null,
32 | expires = 0, // 0 means no expiration.
33 | timeNow = new Date().getTime();
34 |
35 | if (!storageUtil.isSupported()) {
36 | return null;
37 | }
38 |
39 | // If it has an expiration date (has a # other than 0) that has passed, remove it b/c it's invalid data now.
40 | // else parse the storage data and set it for return.
41 | if (localStorage.getItem(key) !== null) {
42 | expires = JSON.parse(localStorage.getItem(key)).expires;
43 |
44 | if (expires !== 0 && expires < timeNow) {
45 | storageUtil.removeItem(key);
46 | }
47 | else {
48 | storageData = JSON.parse(localStorage.getItem(key)).value;
49 | }
50 | }
51 |
52 | return storageData;
53 | };
54 |
55 | /**
56 | Checks if browser localStorage is supported by the current user's browser.
57 | This is used by every method in this utility class so you don't need to use this unless you have special case use for it.
58 | Provided as a public method purely for your convenience.
59 |
60 | @method isSupported
61 | @return {Boolean} True if localStorage is supported, else false.
62 | **/
63 | storageUtil.isSupported = function () {
64 | try {
65 | return localStorage && typeof JSON !== "undefined";
66 | }
67 | catch (e) {
68 | return false;
69 | }
70 | };
71 |
72 | /**
73 | Deletes the requested item from browser localStorage.
74 |
75 | @method removeItem
76 | @param key {String} The name of the key/item to delete from localStorage.
77 | @return {Boolean} True if localStorage is supported, else false.
78 | **/
79 | storageUtil.removeItem = function (key) {
80 | if (!storageUtil.isSupported()) {
81 | return false;
82 | }
83 | localStorage.removeItem(key);
84 | return true;
85 | };
86 |
87 | /**
88 | Stores data in browser localStorage.
89 |
90 | @method setItem
91 | @param key {String} The name of the key to use for this data store in localStorage.
92 | @param value {String} The value/data to store in localStorage.
93 | @param [lifetime] {String} The storage item's TTL (time to live), in SECONDS .
94 | AKA: How long until it expires.
95 | If lifetime is not supplied, the storage item TTL is session-only.
96 | @return {Boolean} True if localStorage is supported, else false.
97 | **/
98 | storageUtil.setItem = function (key, value, lifetime) {
99 | var expireTime = 0,
100 | storageObject = {},
101 | timeNow = new Date().getTime();
102 |
103 | // Can't do shit if localStorage isn't supported.
104 | if (!storageUtil.isSupported()) {
105 | return false;
106 | }
107 |
108 | // First we should remove this key if it already exists.
109 | storageUtil.removeItem(key);
110 |
111 | // If lifetime is specified...
112 | if (lifetime) {
113 | expireTime = lifetime * 1000;
114 | expireTime += timeNow;
115 | }
116 |
117 | // Build our storage object.
118 | storageObject = {
119 | "value": value,
120 | "expires": expireTime
121 | };
122 |
123 | // Do it.
124 | localStorage.setItem(key, JSON.stringify(storageObject));
125 |
126 | return true;
127 | };
128 |
129 |
130 | })(jQuery, PL);
131 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/lighthouse-viewer/images/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/lighthouse-viewer/images/android-chrome-192x192.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/lighthouse-viewer/images/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/lighthouse-viewer/images/android-chrome-512x512.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/lighthouse-viewer/images/lh_logo_bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IBM/page-lab/00aa6167728452031bc76e1e679af9e9b871d7d3/admin/pageaudit/static/report/lighthouse-viewer/images/lh_logo_bg.png
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/lighthouse-viewer/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Lighthouse Viewer",
3 | "short_name": "Lighthouse",
4 | "icons": [
5 | {
6 | "src": "images/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "images/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#304ffe",
17 | "background_color": "#304ffe",
18 | "display": "standalone",
19 | "start_url": "./"
20 | }
21 |
--------------------------------------------------------------------------------
/admin/pageaudit/static/report/lighthouse-viewer/src/polyfills/url-search-params.js:
--------------------------------------------------------------------------------
1 | /*! (C) WebReflection Mit Style License */
2 | var URLSearchParams=URLSearchParams||function(){"use strict";function e(e){return encodeURIComponent(e).replace(i,u)}function t(e){return decodeURIComponent(e.replace(s," "))}function n(e){this[f]=Object.create(null);if(!e)return;e.charAt(0)==="?"&&(e=e.slice(1));for(var n,r,i=(e||"").split("&"),s=0,o=i.length;s {
16 | if (!localStorage) {
17 | // Throw if page didn't provide the metrics we expect. This isn't
18 | // fatal -- the Lighthouse run will continue, but any audits that
19 | // depend on this gatherer will show this error string in the report.
20 | throw new Error('Unable to find localStorage in page');
21 | }
22 | return localStorage;
23 | });
24 | }
25 | }
26 |
27 | module.exports = LocalStorageGather;
28 |
--------------------------------------------------------------------------------
/pageaudit/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "chrome-launcher": "^0.10.2",
4 | "chrome-remote-interface": "^0.26.1",
5 | "commander": "^2.17.1",
6 | "express": "^4.16.3",
7 | "lighthouse": "^3.0.3",
8 | "lighthouse-logger": "^1.0.1",
9 | "nbd": "^0.2.2",
10 | "node-fetch": "^2.2.0",
11 | "redis-jsonify": "^1.2.0",
12 | "rsmq": "^0.9.2",
13 | "shelljs": "^0.8.2",
14 | "ssl-root-cas": "^1.2.5",
15 | "valid-url": "^1.0.9"
16 | },
17 | "name": "PageLab",
18 | "version": "0.0.2",
19 | "description": "A lighthouse wrapper configured for web page testing at scale",
20 | "main": "cluster.js",
21 | "devDependencies": {},
22 | "scripts": {
23 | "test": "npm run test"
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/IBM/page-lab/"
28 | },
29 | "keywords": [
30 | "web",
31 | "performance",
32 | "testing"
33 | ],
34 | "author": "",
35 | "license": "Apache2"
36 | }
37 |
--------------------------------------------------------------------------------