diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7a20d3b1c..c48092cda 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ jobs: strategy: max-parallel: 3 matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - name: checkout diff --git a/CHANGES.rst b/CHANGES.rst index ff1f0a9bd..4ff6112c7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,13 @@ +pywb 2.7.1 changelist +~~~~~~~~~~~~~~~~~~~~~ + +* Add locale-dependent handling of first day of week by @krakan in https://github.com/webrecorder/pywb/pull/781 +* Make filter expressions translatable by @krakan in https://github.com/webrecorder/pywb/pull/783 +* Add title to top frame in framed replay +* Add missing tooltip translation strings +* Fix calendar and timeline rendering for replay URLs without a timestamp +* Update template documentation + pywb 2.7.0 changelist ~~~~~~~~~~~~~~~~~~~~~ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f33e1a9d0..93acf7990 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,13 +51,11 @@ The first time you run this command, it make take some time to build. Changes to the [Vue](https://vuejs.org/) frontend components require rebuilding the Vue bundle (`pywb/static/vue/vueui.js`) to take effect. After making changes to one or more Vue components, you can rebuild the static bundle and view the changes in your development environment like so: ```bash -cd pywb/vueui -yarn run build -cd ../.. -docker compose up -d --force-recreate +./build-vue-ui.sh +docker compose up -d --build --force-recreate ``` -Changes that modify pywb's Python dependencies or the operating system may require rebuilding the container: +Changes that modify pywb's Python dependencies or the operating system also require rebuilding the container: ```bash docker compose up -d --build --force-recreate diff --git a/build-vue-ui.sh b/build-vue-ui.sh new file mode 100755 index 000000000..f6c724422 --- /dev/null +++ b/build-vue-ui.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +CURR_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) + +cd $CURR_DIR/pywb/vueui/ +yarn run build diff --git a/docs/manual/template-guide.rst b/docs/manual/template-guide.rst index 864ad8377..d0e79da48 100644 --- a/docs/manual/template-guide.rst +++ b/docs/manual/template-guide.rst @@ -48,8 +48,7 @@ Base Templates (and supporting templates) File: ``base.html`` -This template includes the HTML added to all other pages, replay and non-replay. Shared JS and CSS includes can be added here. -For theming all pywb UI, it may be useful to modify this template. +This template includes the HTML added to all pages other than framed replay. Shared JS and CSS includes meant for pages other than framed replay can be added here. To customize the default pywb UI across multiple pages, the following additional templates can also be overriden: @@ -61,7 +60,7 @@ can also be overriden: * ``footer.html`` -- Template for adding content as the "footer" of the ``
`` tag of the ``base`` template -Note: The default pywb ``head.html`` and ``footer.html`` are currently blank. They can be populated to customize the rendering, add analytics, etc... as needed. +Note: The default pywb ``head.html`` and ``footer.html`` are currently blank. They can be populated to customize the rendering, add analytics, etc... as needed. Content such as styles or JS code (for example for analytics) must be added to the ``frame_insert.html`` template as well (details on that template below) to also be included in framed replay. The ``base.html`` template also provides five blocks that can be supplied by templates that extend it. @@ -172,9 +171,7 @@ Banner Template File: ``banner.html`` -This template is used to render the banner and is used both in framed replay and frameless replay. - -In framed replay, the template is only rendered in the top/outer frame, while in frameless replay, it is added to every page. +This template is used to render the banner for framed replay. It is rendered only rendered in the top/outer frame. Template variables: @@ -192,7 +189,17 @@ Template variables: * ``{{ ui }}`` - an optional ``ui`` dictionary from ``config.yaml``, if any. -The default banner creates the UI dynamically in JavaScript using Vue. +The default banner creates the UI dynamically in JavaScript using Vue in the ``frame_insert.html`` template. + + +Custom Banner Template +^^^^^^^^^^^^^^^^^^^^^^ + +File: ``custom_banner.html`` + +This template can be used to render a custom banner for frameless replay. It is blank by default. + +In frameless replay, the content of this template is injected into the ``head_insert.html`` template to render the banner. Head Insert Template @@ -204,7 +211,7 @@ This template represents the HTML injected into every replay page to add support This template is part of the core pywb replay, and modifying this template is not recommended. -For customizing the banner, modify the ``banner.html`` template instead. +For customizing the banner, modify the ``banner.html`` (framed replay) or ``custom_banner.html`` (frameless replay) template instead. Top Frame Template @@ -221,16 +228,21 @@ This template is responsible for creating the iframe that will render the conten This template only renders the banner and is designed *not* to set the encoding to allow the browser to 'detect' the encoding for the containing iframe. For this reason, the template should only contain ASCII text, and %-encode any non-ASCII characters. +Content such as analytics code that is desired in the top frame of framed replay pages should be added to this template. + Template variables: * ``{{ url }}`` - the URL being replayed. +* ``{{ timestamp }}`` - the timestamp being replayed, e.g. ``20211226`` in ``http://localhost:8080/pywb/20211226/mp_/https://example.com/`` + * ``{{ wb_url }}`` - A complete ``WbUrl`` object, which contains the ``url``, ``timestamp`` and ``mod`` properties, representing the replay url. * ``{{ wb_prefix }}`` - the collection prefix, e.g. ``http://localhost:8080/pywb/`` * ``{{ is_proxy }}`` - set to true if page is being loaded via an HTTP/S proxy (checks if WSGI env has ``wsgiprox.proxy_host`` set) +* ``{{ ui }}`` - an optional ``ui`` dictionary from ``config.yaml``, if any. .. _custom-top-frame: diff --git a/pywb/rewrite/templateview.py b/pywb/rewrite/templateview.py index 5dfce5fa0..3c5c1d94c 100644 --- a/pywb/rewrite/templateview.py +++ b/pywb/rewrite/templateview.py @@ -405,10 +405,9 @@ def get_top_frame(self, wb_url, embed_url = wb_url.to_str(mod=replay_mod) + timestamp = '' if wb_url.timestamp: timestamp = wb_url.timestamp - else: - timestamp = timestamp_now() is_proxy = 'wsgiprox.proxy_host' in env diff --git a/pywb/static/query.js b/pywb/static/query.js index cae02cae7..fd0a04502 100644 --- a/pywb/static/query.js +++ b/pywb/static/query.js @@ -57,14 +57,6 @@ function RenderCalendar(init) { }; // regex for extracting the filter constraints and filter mods to human explanation this.filterRE = /filter([^a-z]+)([a-z]+):(.+)/i; - this.filterMods = { - '=': 'Contains', - '==': 'Matches Exactly', - '=~': 'Matches Regex', - '=!': 'Does Not Contains', - '=!=': 'Is Not', - '=!~': 'Does Not Begins With' - }; this.text = init.text; this.versionString = null; } @@ -433,7 +425,6 @@ RenderCalendar.prototype.createContainers = function() { return; } // create the advanced results query info DOM structure - var forString = ' for '; var forElems; if (this.queryInfo.searchParams.matchType) { @@ -503,7 +494,7 @@ RenderCalendar.prototype.createContainers = function() { { tag: 'p', className: 'text-center mb-0 mt-1', - innerText: 'Filtering by' + innerText: filteringBy }, { tag: 'ul', @@ -950,7 +941,7 @@ RenderCalendar.prototype.niceFilterDisplay = function() { filterList.push({ tag: 'li', className: 'list-group-item', - innerText: match[2] + ' ' + this.filterMods[match[1]] + ' ' + match[3] + innerText: match[2] + ' ' + filterMods[match[1]] + ' "' + match[3] + '"' }); } } diff --git a/pywb/static/search.js b/pywb/static/search.js index dd189bb9a..eafa0023d 100644 --- a/pywb/static/search.js +++ b/pywb/static/search.js @@ -1,14 +1,6 @@ var dtRE = /^\d{4,14}$/; var didSetWasValidated = false; var showBadDateTimeClass = 'show-optional-bad-input'; -var filterMods = { - '=': 'Contains', - '==': 'Matches Exactly', - '=~': 'Matches Regex', - '=!': 'Does Not Contains', - '=!=': 'Is Not', - '=!~': 'Does Not Begins With' -}; var elemIds = { filtering: { @@ -65,7 +57,7 @@ function makeCheckDateRangeChecker(dtInputId, dtBadNotice) { function createAndAddNoFilter(filterList) { var nothing = document.createElement('li'); - nothing.innerText = 'No Filter'; + nothing.innerText = noFilter; nothing.id = elemIds.filtering.nothing; filterList.appendChild(nothing); } @@ -78,19 +70,24 @@ function addFilter(event) { if (!expr) return; var filterExpr = 'filter' + modifier + by + ':' + expr; var filterList = document.getElementById(elemIds.filtering.list); + var previousFilters = filterList.children; + for (var i = 0; i < previousFilters.length; ++i) { + var filterData = previousFilters[i].dataset; + if (filterData && filterData.filter && filterData.filter == filterExpr) return; + } var filterNothing = document.getElementById(elemIds.filtering.nothing); if (filterNothing) { filterList.removeChild(filterNothing); } var li = document.createElement('li'); li.innerText = - 'By ' + by[0].toUpperCase() + by.substr(1) + ' ' + filterMods[modifier] + - ' ' + - expr; + ' "' + + expr + + '"'; li.dataset.filter = filterExpr; var nukeButton = document.createElement('button'); nukeButton.type = 'button'; @@ -110,6 +107,7 @@ function addFilter(event) { }; li.appendChild(nukeButton); filterList.appendChild(li); + return true; } function clearFilters(event) { @@ -166,6 +164,17 @@ function validateFields(form) { } } +function submitForm(event, form, searchURLInput) { + event.preventDefault(); + event.stopPropagation(); + var url = searchURLInput.value; + if (!url) { + validateFields(form); + return; + } + performQuery(url); +} + $(document).ready(function() { $('[data-toggle="tooltip"]').tooltip({ container: 'body', @@ -184,16 +193,18 @@ $(document).ready(function() { var searchURLInput = document.getElementById(elemIds.url); var form = document.getElementById(elemIds.form); form.addEventListener('submit', function(event) { - event.preventDefault(); - event.stopPropagation(); - var url = searchURLInput.value; - if (!url) { - validateFields(form); - return; - } - performQuery(url); + submitForm(event, form, searchURLInput); }); document.getElementById(elemIds.advancedOptions).onclick = function() { validateFields(form); } + var filteringExpression = document.getElementById(elemIds.filtering.expression); + filteringExpression.addEventListener("keypress", function(event) { + if (event.key === "Enter") { + event.preventDefault(); + if (! addFilter()) { + submitForm(event, form, searchURLInput); + } + } + }); }); diff --git a/pywb/static/vue/vueui.js b/pywb/static/vue/vueui.js index 333f22316..2ba3e8857 100644 --- a/pywb/static/vue/vueui.js +++ b/pywb/static/vue/vueui.js @@ -6,12 +6,15 @@ var VueUI = (function (exports) { static getLocale() { // get via public static method return PywbI18N.#locale; } + static firstDayOfWeek = 1; static init = (locale, config) => { if (PywbI18N.instance) { throw new Error('cannot instantiate PywbI18N twice'); } PywbI18N.#locale = locale; PywbI18N.instance = new PywbI18N(config); + let intlLocale = new Intl.Locale(PywbI18N.getLocale()); + if ('weekInfo' in intlLocale) PywbI18N.firstDayOfWeek = intlLocale.weekInfo.firstDay % 7; } // PywbI18N expects from the i18n string source to receive months SHORT and LONG names in the config like this: @@ -39,7 +42,8 @@ var VueUI = (function (exports) { return decodeURIComponent(this.config[id+'_'+type]) } getWeekDays(type='long') { - return ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'].map(d => this.getWeekDay(d, type)); + let weekDays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; + return weekDays.concat(weekDays).slice(PywbI18N.firstDayOfWeek, PywbI18N.firstDayOfWeek + 7).map(d => this.getWeekDay(d, type)); } getText(id, embeddedVariableStrings=null) { const translated = decodeURIComponent(this.config[id] || id); @@ -1413,8 +1417,8 @@ var VueUI = (function (exports) { const days = []; // Get days in month, and days in the complete weeks before first day and after last day const [firstDay, lastDay] = this.month.getChildrenRange(); - const daysBeforeFirst = (new Date(this.year.id, this.month.id-1, firstDay)).getDay(); - const daysAfterLastDay = (6 - (new Date(this.year.id, this.month.id-1, lastDay)).getDay()); + const daysBeforeFirst = (7 + (new Date(this.year.id, this.month.id-1, firstDay)).getDay() - PywbI18N.firstDayOfWeek) % 7; + const daysAfterLastDay = (6 - (new Date(this.year.id, this.month.id-1, lastDay)).getDay() + PywbI18N.firstDayOfWeek) % 7; for(let i=0; i