From b303a9092d66313970d3e4bd030e1d6178c874fd Mon Sep 17 00:00:00 2001 From: mutantsan Date: Tue, 23 Apr 2024 15:39:02 +0300 Subject: [PATCH] feature: ap_doi plugin --- ckanext/ap_doi/__init__.py | 0 ckanext/ap_doi/collection.py | 141 +++++++++++ ckanext/ap_doi/config.py | 9 + ckanext/ap_doi/config_declaration.yaml | 44 ++++ ckanext/ap_doi/config_schema.yaml | 60 +++++ ckanext/ap_doi/const.py | 1 + ckanext/ap_doi/logic/action.py | 229 ++++++++++++++++++ ckanext/ap_doi/logic/auth.py | 9 + ckanext/ap_doi/logic/schema.py | 15 ++ ckanext/ap_doi/plugin.py | 123 ++++++++++ ckanext/ap_doi/presets.yaml | 11 + ckanext/ap_doi/templates/ap_doi/list.html | 18 ++ ckanext/ap_doi/templates/ap_doi/pager.html | 0 ckanext/ap_doi/templates/ap_doi/record.html | 17 ++ .../scheming/form_snippets/hidden.html | 4 + .../ap_doi/templates/user/dashboard_doi.html | 59 +++++ ckanext/ap_doi/utils.py | 147 +++++++++++ ckanext/ap_doi/views.py | 124 ++++++++++ ckanext/ap_example/plugin.py | 2 +- ckanext/ap_example/views.py | 3 +- ckanext/ap_main/assets/js/vendor/htmx.min.js | 3 +- .../templates/admin_panel/panel_links.html | 4 +- ckanext/ap_main/views/generics.py | 2 +- setup.cfg | 5 +- 24 files changed, 1022 insertions(+), 8 deletions(-) create mode 100644 ckanext/ap_doi/__init__.py create mode 100644 ckanext/ap_doi/collection.py create mode 100644 ckanext/ap_doi/config.py create mode 100644 ckanext/ap_doi/config_declaration.yaml create mode 100644 ckanext/ap_doi/config_schema.yaml create mode 100644 ckanext/ap_doi/const.py create mode 100644 ckanext/ap_doi/logic/action.py create mode 100644 ckanext/ap_doi/logic/auth.py create mode 100644 ckanext/ap_doi/logic/schema.py create mode 100644 ckanext/ap_doi/plugin.py create mode 100644 ckanext/ap_doi/presets.yaml create mode 100644 ckanext/ap_doi/templates/ap_doi/list.html create mode 100644 ckanext/ap_doi/templates/ap_doi/pager.html create mode 100644 ckanext/ap_doi/templates/ap_doi/record.html create mode 100644 ckanext/ap_doi/templates/scheming/form_snippets/hidden.html create mode 100644 ckanext/ap_doi/templates/user/dashboard_doi.html create mode 100644 ckanext/ap_doi/utils.py create mode 100644 ckanext/ap_doi/views.py diff --git a/ckanext/ap_doi/__init__.py b/ckanext/ap_doi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/ap_doi/collection.py b/ckanext/ap_doi/collection.py new file mode 100644 index 0000000..afcc6f3 --- /dev/null +++ b/ckanext/ap_doi/collection.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from dominate import tags + +import ckan.plugins.toolkit as tk + +import ckanext.ap_main.collection.base as collection_base + +from ckanext.collection.utils import ApiData +from ckanext.collection.types import InputFilter, ButtonFilter, SelectFilter +from ckanext.collection.utils import Filters + +from ckanext.ap_doi.utils import DOIProblemPackageData + + +def row_dictizer( + serializer: collection_base.ApHtmxTableSerializer, row: DOIProblemPackageData +): + row["bulk-action"] = row["id"] # type: ignore + + return row + + +class ApDOICollection(collection_base.ApCollection): + SerializerFactory = collection_base.ApHtmxTableSerializer.with_attributes( + row_dictizer=row_dictizer, + # record_template="ap_doi/record.html", + # pager_template="ap_doi/pager.html", + ) + + ColumnsFactory = collection_base.ApColumns.with_attributes( + names=[ + "bulk-action", + "title", + "doi_status", + "identifier", + "timestamp", + "published", + "row_actions", + ], + sortable={}, + labels={ + "bulk-action": tk.literal( + tags.input_( + type="checkbox", + name="bulk_check", + id="bulk_check", + data_module="ap-bulk-check", + data_module_selector='input[name="entity_id"]', + ) + ), + "title": "Title", + "doi_status": "Status", + "identifier": "DOI", + "timestamp": "Timestamp", + "published": "Publish Date", + "row_actions": "Actions", + }, + width={"title": "15%", "doi_status": "10%", "row_actions": "20%"}, + serializers={ + "timestamp": [("date", {})], + "published": [("date", {})], + }, + ) + + DataFactory = ApiData.with_attributes( + action="ap_doi_get_packages_doi", + ) + + FiltersFactory = Filters.with_attributes( + static_actions=[ + collection_base.BulkAction( + name="bulk-action", + type="bulk_action", + options={ + "label": "Action", + "options": [ + {"value": "1", "text": "Update DOI"}, + ], + }, + ), + collection_base.RowAction( + name="view", + type="row_action", + options={ + "endpoint": "doi_dashboard.create_or_update_doi", + "label": "Update DOI", + "params": { + "package_id": "$id", + }, + }, + ), + collection_base.RowAction( + name="view", + type="row_action", + options={ + "endpoint": "ap_content.entity_proxy", + "label": "View", + "params": { + "entity_id": "$name", + "entity_type": "$type", + "view": "read", + }, + }, + ), + ], + static_filters=[ + InputFilter( + name="q", + type="input", + options={ + "label": "Search", + "placeholder": "Search", + }, + ), + SelectFilter( + name="doi_status", + type="select", + options={ + "label": "Status", + "options": [ + {"value": "", "text": "Any"}, + {"value": "Published", "text": "Published"}, + {"value": "Missing", "text": "Missing"}, + {"value": "Outdated", "text": "Outdated"}, + ], + }, + ), + ButtonFilter( + name="type", + type="button", + options={ + "label": "Clear", + "type": "button", + "attrs": { + "onclick": "$(this).closest('form').find('input,select').val('').prevObject[0].requestSubmit()" + }, + }, + ), + ], + ) diff --git a/ckanext/ap_doi/config.py b/ckanext/ap_doi/config.py new file mode 100644 index 0000000..464a22d --- /dev/null +++ b/ckanext/ap_doi/config.py @@ -0,0 +1,9 @@ +import ckan.plugins.toolkit as tk + + +def is_mock_api_calls() -> bool: + return tk.asbool(tk.config.get("ckanext.ap_doi.mock_api_calls")) + + +def get_doi_prefix() -> str: + return tk.config.get("ckanext.doi.prefix") diff --git a/ckanext/ap_doi/config_declaration.yaml b/ckanext/ap_doi/config_declaration.yaml new file mode 100644 index 0000000..9d67a82 --- /dev/null +++ b/ckanext/ap_doi/config_declaration.yaml @@ -0,0 +1,44 @@ +version: 1 +groups: + - annotation: Admin panel example + options: + # The following keys are declared here, because ckanext-doi doesn't provide config declaration + + - key: ckanext.doi.account_name + required: true + description: Your DataCite Repository account + editable: true + + - key: ckanext.doi.account_password + required: true + description: Your DataCite Repository account password + editable: true + + - key: ckanext.doi.prefix + required: true + description: The prefix taken from your DataCite Repository account (from your test account if running in test mode) + editable: true + + - key: ckanext.doi.publisher + required: true + description: The name of the institution publishing the DOI + editable: true + + - key: ckanext.doi.test_mode + description: Enable dev/test mode + required: true + description: If test mode is set to true, the DOIs will use the DataCite test site. The test site uses a separate account, so you must also change your credentials and prefix. + editable: true + + - key: ckanext.doi.site_url + description: Used to build the link back to the dataset + editable: true + + - key: ckanext.doi.site_title + description: Site title to use in the citation + editable: true + + # AP_DOI config options + - key: ckanext.ap_doi.mock_api_calls + description: If enabled, the extension will not make any API calls to DataCite + editable: true diff --git a/ckanext/ap_doi/config_schema.yaml b/ckanext/ap_doi/config_schema.yaml new file mode 100644 index 0000000..cfa78d9 --- /dev/null +++ b/ckanext/ap_doi/config_schema.yaml @@ -0,0 +1,60 @@ +scheming_version: 2 +schema_id: ap_doi_config +about: Configuration options for the DOI extension + +fields: + - field_name: ckanext.ap_doi.mock_api_calls + label: Mock API calls + description: If enabled, the extension will not make any API calls to DataCite + validators: default(true), one_of([true, false]) + preset: select + required: true + choices: + - value: true + label: Enabled + - value: false + label: Disabled + + - field_name: ckanext.doi.account_name + label: Account name + required: true + description: Your DataCite Repository account + form_snippet: text.html + display_snippet: text.html + + - field_name: ckanext.doi.account_password + label: Account password + required: true + description: Your DataCite Repository account password + input_type: password + + - field_name: ckanext.doi.prefix + label: DOI prefix + required: true + description: The prefix taken from your DataCite Repository account (from your test account if running in test mode) + + - field_name: ckanext.doi.publisher + label: Publisher + required: true + description: The name of the institution publishing the DOI + + - field_name: ckanext.doi.test_mode + label: Test mode + required: true + description: If test mode is set to true, the DOIs will use the DataCite test site. The test site uses a separate account, so you must also change your credentials and prefix. + validators: default(true), one_of([true, false]) + preset: select + required: true + choices: + - value: true + label: Enabled + - value: false + label: Disabled + + - field_name: ckanext.doi.site_url + label: Site URL + description: Used to build the link back to the dataset + + - field_name: ckanext.doi.site_title + label: Site title + description: Site title to use in the citation diff --git a/ckanext/ap_doi/const.py b/ckanext/ap_doi/const.py new file mode 100644 index 0000000..b565ee6 --- /dev/null +++ b/ckanext/ap_doi/const.py @@ -0,0 +1 @@ +DOI_FLAKE_NAME = "ckanext:ap_doi:doi:" diff --git a/ckanext/ap_doi/logic/action.py b/ckanext/ap_doi/logic/action.py new file mode 100644 index 0000000..4501be6 --- /dev/null +++ b/ckanext/ap_doi/logic/action.py @@ -0,0 +1,229 @@ +from __future__ import annotations + +import random +import string +from datetime import datetime as dt +from datetime import timezone +from typing import Any + +from datacite.errors import DataCiteError + +import ckan.plugins.toolkit as tk +from ckan.logic import validate + +import ckanext.doi.model.crud as doi_crud +from ckanext.doi.lib.api import DataciteClient +from ckanext.doi.lib.metadata import build_metadata_dict, build_xml_dict +from ckanext.doi.model.doi import DOI + +import ckanext.ap_doi.logic.schema as schema +from ckanext.ap_doi import const, config +from ckanext.ap_doi.utils import DOIProblemPackageData +from ckanext.ap_doi.utils import ( + get_doi_to_update, + get_packages_to_update, + package_already_in_flake, + remove_package_from_flake, +) + + +@tk.side_effect_free +@validate(schema.ap_doi_get_packages_doi) +def ap_doi_get_packages_doi( + context: Any, data_dict: dict[str, Any] +) -> list[DOIProblemPackageData]: + """Retrieve packages data along with DOI identifiers.""" + tk.check_access("ap_doi_get_packages_doi", context, data_dict) + + model = context["model"] + packages_to_update = get_packages_to_update(const.DOI_FLAKE_NAME) + + results = ( + model.Session.query( + model.Package.id.label("package_id"), + model.Package.name.label("package_name"), + model.Package.title.label("package_title"), + model.Package.type.label("package_type"), + model.Package.metadata_modified.label("metadata_modified"), + DOI.published.label("published"), + DOI.identifier.label("identifier"), + ) + .join(DOI, model.Package.id == DOI.package_id, isouter=True) + .filter(model.Package.type != "harvest") + .filter( + model.Package.id.notin_( + ( + model.Session.query(model.PackageExtra.package_id.label("id")) + .filter(model.PackageExtra.key == "data_source") + .filter(model.PackageExtra.value != "") + .subquery() + ) + ) + ) + .all() + ) + + results = _prepare_problem_package_data(results, packages_to_update) + return filter_dois(results, data_dict) + + +def _prepare_problem_package_data( + results: Any, + packages_to_update: list[DOIProblemPackageData], +) -> list[DOIProblemPackageData]: + packages_data: list[DOIProblemPackageData] = [] + outdated_ids = [package["id"] for package in packages_to_update] + + for row in results: + if row.package_id in outdated_ids: + continue + + doi_status = "Missing" + + if row.published and row.published < row.metadata_modified: + doi_status = "Outdated" + elif row.published: + doi_status = "Published" + + packages_data.append( + { + "id": row.package_id, + "name": row.package_name, + "title": row.package_title, + "doi_status": doi_status, + "published": row.published, + "identifier": row.identifier, + "timestamp": row.metadata_modified, + "type": row.package_type, + } + ) + + for package in packages_to_update: + if package not in packages_data: + packages_data.append(package) + + # Sort packages_data, prioritizing 'Outdated' status + return sorted( + packages_data, + key=lambda x: (x["doi_status"] == "Outdated", x["timestamp"]), + # Reverse to get 'Outdated' first + reverse=True, + ) + + +def filter_dois( + results: list[DOIProblemPackageData], data_dict: dict[str, Any] +) -> list[DOIProblemPackageData]: + """Filter out packages that do not have a DOI.""" + + if q := tk.request.args.get("ap-doi:q"): + results = [result for result in results if q.lower() in result["title"].lower()] + + if doi_status := tk.request.args.get("ap-doi:doi_status"): + results = [result for result in results if doi_status == result["doi_status"]] + + return results + + +def ap_doi_update_doi(context: Any, data_dict: dict[str, Any]) -> dict[str, Any]: + """Action to update a DOI.""" + tk.check_access("ap_doi_update_doi", context, data_dict) + + package_id = data_dict.get("package_id") + + if not package_id: + raise tk.ValidationError({"package_id": "Missing package_id for DOI update"}) + + if config.is_mock_api_calls(): + errors = _mock_update_doi_metadata(context, package_id) + else: + doi_to_update = get_doi_to_update(context["model"], package_id) + pkg_dict = tk.get_action("package_show")({}, {"id": doi_to_update.package_id}) + errors = _update_doi_metadata(pkg_dict, doi_to_update) + + if not errors and package_already_in_flake(const.DOI_FLAKE_NAME, package_id): + remove_package_from_flake(const.DOI_FLAKE_NAME, package_id) + + return { + "status": "success" if not errors else "error", + # "status": "success", + "message": "DOI update completed", + "errors": errors, + } + + +def _mock_update_doi_metadata(context: Any, package_id: dict[str, Any]): + if doi_crud.DOIQuery.read_package(package_id): + context["model"].Session.query(DOI).filter(DOI.package_id == package_id).update( + {"published": dt.now(timezone.utc).isoformat()} + ) + else: + alphabet = string.ascii_lowercase + string.digits + doi = f"{config.get_doi_prefix()}/{''.join(random.choices(alphabet, k=8))}" + doi_crud.DOIQuery.create(doi, package_id) + + context["model"].Session.commit() + + return [] + + +def _update_doi_metadata(pkg_dict: dict[str, Any], doi_to_update: DOI): + """Update the DOI metadata and handle DOI creation or update as necessary.""" + title = pkg_dict.get("title", doi_to_update.package_id) + + if not pkg_dict.get("author"): + _add_author_to_pkg_dict(pkg_dict) + + metadata_dict = build_metadata_dict(pkg_dict) + xml_dict = build_xml_dict(metadata_dict) + return _handle_doi_creation_or_update(doi_to_update, xml_dict, title) + + +def _add_author_to_pkg_dict(pkg_dict: dict[str, Any]): + """Set the author full name in the package dictionary.""" + creator = tk.get_action("user_show")({}, {"id": pkg_dict["creator_user_id"]}) + full_name = creator.get("fullname") + name = creator.get("name") + pkg_dict["author"] = full_name or name + + +def _handle_doi_creation_or_update( + doi_to_update: DOI, xml_dict: dict[str, Any], title: str +): + """Handle DOI creation or update based on the DOI's publication status.""" + errors = [] + client = DataciteClient() + + if doi_to_update.published is None: + _create_doi(client, doi_to_update, xml_dict) + else: + _update_existing_doi(client, doi_to_update, xml_dict, title, errors) + return errors + + +def _create_doi(client: DataciteClient, doi_to_update: DOI, xml_dict: dict[str, Any]): + """Create a new DOI with the given metadata.""" + client.set_metadata(doi_to_update.identifier, xml_dict) + client.mint_doi(doi_to_update.identifier, doi_to_update.package_id) + + +def _update_existing_doi( + client: DataciteClient, + doi_to_update: DOI, + xml_dict: dict[str, Any], + title: str, + errors: list[str], +): + """Update an existing DOI if the metadata has changed.""" + same = client.check_for_update(doi_to_update.identifier, xml_dict) + if not same: + try: + client.set_metadata(doi_to_update.identifier, xml_dict) + except DataCiteError as e: + errors.append( + f'Error while updating "{title}"' + f" (DOI {doi_to_update.identifier}): {str(e)}" + ) + else: + errors.append(f'"{title}" is already up to date') + remove_package_from_flake(const.DOI_FLAKE_NAME, doi_to_update.package_id) diff --git a/ckanext/ap_doi/logic/auth.py b/ckanext/ap_doi/logic/auth.py new file mode 100644 index 0000000..409903f --- /dev/null +++ b/ckanext/ap_doi/logic/auth.py @@ -0,0 +1,9 @@ +from __future__ import annotations + + +def ap_doi_get_packages_doi(context, data_dict): + return {"success": False} + + +def ap_doi_update_doi(context, data_dict): + return {"success": False} diff --git a/ckanext/ap_doi/logic/schema.py b/ckanext/ap_doi/logic/schema.py new file mode 100644 index 0000000..7ee339a --- /dev/null +++ b/ckanext/ap_doi/logic/schema.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from typing import Any, Dict + +from ckan.logic.schema import validator_args + +Schema = Dict[str, Any] + + +@validator_args +def ap_doi_get_packages_doi(unicode_safe, default) -> Schema: + return { + "doi_status": [default(""), unicode_safe], + "q": [default(""), unicode_safe], + } diff --git a/ckanext/ap_doi/plugin.py b/ckanext/ap_doi/plugin.py new file mode 100644 index 0000000..8debe39 --- /dev/null +++ b/ckanext/ap_doi/plugin.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +from typing import Any +from os import path + +from yaml import safe_load + +import ckan.plugins as p +import ckan.logic as logic +import ckan.plugins.toolkit as tk +from ckan.types import SignalMapping +from ckan.config.declaration import Declaration, Key + +from ckanext.doi.plugin import DOIPlugin + +import ckanext.ap_main.types as ap_types + +from ckanext.collection.interfaces import CollectionFactory, ICollection +from ckanext.ap_doi.collection import ApDOICollection + +import ckanext.ap_doi.const as const +import ckanext.ap_doi.utils as utils +import ckanext.ap_doi.config as config + + +@tk.blanket.blueprints +@tk.blanket.actions +@tk.blanket.auth_functions +class AdminPanelDoiPlugin(p.SingletonPlugin): + p.implements(p.IConfigurer) + p.implements(p.IPackageController, inherit=True) + p.implements(p.ISignal) + p.implements(p.IConfigDeclaration) + p.implements(ICollection, inherit=True) + + # IConfigurer + + def update_config(self, config_: tk.CKANConfig): + tk.add_template_directory(config_, "templates") + tk.add_public_directory(config_, "public") + tk.add_resource("assets", "ap_doi") + + # IPackageController + + def after_dataset_show(self, context: Any, pkg_dict: dict[str, Any]): + if not pkg_dict.get("author"): + pkg_dict = utils.set_package_author(pkg_dict) + + return pkg_dict + + def after_dataset_update(self, context: Any, pkg_dict: dict[str, Any]): + if not pkg_dict.get("author"): + pkg_dict = utils.set_package_author(pkg_dict) + + doi_to_update = utils.get_doi_to_update(context["model"], pkg_dict["id"]) + flake_exists = utils.package_already_in_flake( + const.DOI_FLAKE_NAME, pkg_dict["id"] + ) + is_old_doi = doi_to_update.published is not None + + # We need to create flake only for old DOIs, + # because if it is a newly generated, + # it will be shown as a package without DOI, + # and we don't have to check its metadata updates + if not flake_exists and is_old_doi: + utils.add_package_to_flake(const.DOI_FLAKE_NAME, pkg_dict["id"]) + + return pkg_dict + + # ISignal + + def get_signal_subscriptions(self) -> SignalMapping: + return { + tk.signals.ckanext.signal("ap_main:collect_config_sections"): [ + collect_config_sections_subscriber, + ], + } + + # IConfigDeclaration + + def declare_config_options(self, declaration: Declaration, key: Key): + logic.clear_validators_cache() + + with open(path.dirname(__file__) + "/config_declaration.yaml") as file: + data_dict = safe_load(file) + + return declaration.load_dict(data_dict) + + # ICollection + + def get_collection_factories(self) -> dict[str, CollectionFactory]: + return {"ap-doi": ApDOICollection} + + +def collect_config_sections_subscriber(sender: None): + return ap_types.SectionConfig( + name="DOI", + configs=[ + ap_types.ConfigurationItem( + name="Dashboard", + blueprint="doi_dashboard.list", + ), + ap_types.ConfigurationItem( + name="DOI settings", + blueprint="doi_dashboard.config", + ), + ], + ) + + +class ApDOIPlugin(DOIPlugin): + def after_dataset_create(self, context, pkg_dict): + if config.is_mock_api_calls(): + return + + super(ApDOIPlugin, self).after_dataset_create(context, pkg_dict) + + ## IPackageController + def after_dataset_update(self, context, pkg_dict): + if config.is_mock_api_calls(): + return + + super(ApDOIPlugin, self).after_dataset_update(context, pkg_dict) diff --git a/ckanext/ap_doi/presets.yaml b/ckanext/ap_doi/presets.yaml new file mode 100644 index 0000000..fbd034f --- /dev/null +++ b/ckanext/ap_doi/presets.yaml @@ -0,0 +1,11 @@ +scheming_presets_version: 2 +about: AP DOI presets +about_url: http://github.com/ckan/ckanext-scheming#preset + +presets: + - preset_name: ap_doi + values: + field_name: doi + label: Dataset DOI + form_snippet: hidden.html + display_snippet: text.html diff --git a/ckanext/ap_doi/templates/ap_doi/list.html b/ckanext/ap_doi/templates/ap_doi/list.html new file mode 100644 index 0000000..09dd631 --- /dev/null +++ b/ckanext/ap_doi/templates/ap_doi/list.html @@ -0,0 +1,18 @@ +{% extends 'admin_panel/base.html' %} + +{% block ap_main_class %} ap-doi {% endblock %} + +{% block breadcrumb_content %} +
  • {% link_for _("DOI"), request.endpoint %}
  • +{% endblock breadcrumb_content %} + +{% block ap_content %} + {% if collection.data.total %} + {{ collection.serializer.render() | safe }} + {% else %} +

    + {{ _("No packages with outdated DOI found") }} + {{ _("Clear the search") }} +

    + {% endif %} +{% endblock ap_content %} diff --git a/ckanext/ap_doi/templates/ap_doi/pager.html b/ckanext/ap_doi/templates/ap_doi/pager.html new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/ap_doi/templates/ap_doi/record.html b/ckanext/ap_doi/templates/ap_doi/record.html new file mode 100644 index 0000000..955a4d0 --- /dev/null +++ b/ckanext/ap_doi/templates/ap_doi/record.html @@ -0,0 +1,17 @@ +{% set data = collection.serializer.dictize_row(record) if collection.serializer.ensure_dictized else record %} + +
    {{ data |tojson(4) }}
    + + + {% block rows %} + {% for column in collection.columns.names if column in collection.columns.visible -%} + + {% block value scoped %} +
    + {{ data[column] }} +
    + {% endblock %} + + {%- endfor %} + {% endblock rows%} + diff --git a/ckanext/ap_doi/templates/scheming/form_snippets/hidden.html b/ckanext/ap_doi/templates/scheming/form_snippets/hidden.html new file mode 100644 index 0000000..7320c7b --- /dev/null +++ b/ckanext/ap_doi/templates/scheming/form_snippets/hidden.html @@ -0,0 +1,4 @@ +{# Form snippet for fields that should be hidden from the user, but we still want + their contents to be saved and submitted with the form #} + + diff --git a/ckanext/ap_doi/templates/user/dashboard_doi.html b/ckanext/ap_doi/templates/user/dashboard_doi.html new file mode 100644 index 0000000..7e41e8b --- /dev/null +++ b/ckanext/ap_doi/templates/user/dashboard_doi.html @@ -0,0 +1,59 @@ +{% extends "user/dashboard.html" %} + +{% block primary_content_inner %} +
    +

    + {% block page_heading %} + {{ _('DATASETS WITH DOI ISSUE') }} + {% endblock %} +

    + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
    + {% for category, message in messages %} +
    + × +

    {{ message }}

    +
    + {% endfor %} +
    + {% endif %} + {% endwith %} + +
    +{% endblock %} diff --git a/ckanext/ap_doi/utils.py b/ckanext/ap_doi/utils.py new file mode 100644 index 0000000..6eaa553 --- /dev/null +++ b/ckanext/ap_doi/utils.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +from datetime import datetime +from typing import Any, Optional, TypedDict, cast + +from ckanext.doi.lib.api import DataciteClient +from ckanext.doi.model.doi import DOI + +import ckan.types as types +import ckan.plugins.toolkit as tk + + +class DOIFlakesData(TypedDict): + require_doi_update: list[DOIProblemPackageData] + + +class DOIProblemPackageData(TypedDict): + id: str + name: str + title: str + doi_status: Optional[str] + timestamp: datetime + type: str + published: str + identifier: str + + +def prepare_context() -> types.Context: + return cast(types.Context, {"ignore_auth": True}) + + +def store_data_in_flake(flake_name: str, data: Any) -> dict[str, Any]: + """Save the serializable data into the flakes table.""" + return tk.get_action("flakes_flake_override")( + prepare_context(), + {"author_id": None, "name": flake_name, "data": data}, + ) + + +def get_data_from_flake(flake_name: str) -> dict[str, Any]: + """Retrieve a previously stored data from the flake.""" + try: + return tk.get_action("flakes_flake_lookup")( + prepare_context(), + {"author_id": None, "name": flake_name}, + ) + except tk.ObjectNotFound: + return tk.get_action("flakes_flake_create")( + prepare_context(), + {"author_id": None, "name": flake_name, "data": {}}, + ) + + +def package_already_in_flake(flake_name: str, package_id: str) -> bool: + packages_to_update = get_packages_to_update(flake_name) + + if not packages_to_update: + return False + + for package in packages_to_update: + if package["id"] == package_id: + return True + + return False + + +def add_package_to_flake(flake_name: str, package_id: str) -> None: + packages_to_update: list[DOIProblemPackageData] = get_packages_to_update(flake_name) + package_dict = tk.get_action("package_show")({}, {"id": package_id}) + + problem_package_dict: DOIProblemPackageData = { + "id": package_dict["id"], + "name": package_dict["name"], + "title": package_dict["title"], + "doi_status": "Outdated", + "timestamp": package_dict["metadata_modified"], + "identifier": "", + "published": "", + "type": package_dict["type"], + } + + data: DOIFlakesData = {"require_doi_update": [problem_package_dict]} + + if not packages_to_update: + store_data_in_flake(flake_name, data) + return + + packages_to_update.append(problem_package_dict) + + store_data_in_flake(flake_name, {"require_doi_update": packages_to_update}) + + +def remove_package_from_flake(flake_name: str, package_id: str) -> None: + packages_to_update = get_packages_to_update(flake_name) + + if not packages_to_update: + return + + store_data_in_flake( + flake_name, + { + "require_doi_update": [ + package for package in packages_to_update if package["id"] != package_id + ] + }, + ) + + +def get_packages_to_update(flake_name: str) -> list[DOIProblemPackageData]: + flake = get_data_from_flake(flake_name) + + doi_data = flake["data"].get("require_doi_update") + + if not doi_data: + return [] + + return doi_data + + +def get_doi_to_update(model: Any, package_id: str) -> DOI: + """Retrieve the DOI object to be updated, + or create a new one if it doesn't exist.""" + doi_to_update = model.Session.query(DOI).filter_by(package_id=package_id).first() + + if not doi_to_update: + identifier = DataciteClient().generate_doi() + doi_to_update = DOI(package_id=package_id, identifier=identifier) + + return doi_to_update + + +def set_package_author(pkg_dict: dict[str, Any]): + creator_id = pkg_dict.get("creator_user_id") + + if not creator_id: + package = tk.get_action("package_show")({}, {"id": pkg_dict["id"]}) + creator_id = package.get("creator_user_id") + + if package.get("author"): + pkg_dict["author"] = package.get("author") + return pkg_dict + + creator = tk.get_action("user_show")({}, {"id": creator_id}) + + pkg_dict["author"] = creator.get("fullname") or creator.get("name") + + return pkg_dict diff --git a/ckanext/ap_doi/views.py b/ckanext/ap_doi/views.py new file mode 100644 index 0000000..2f8eced --- /dev/null +++ b/ckanext/ap_doi/views.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +import logging +from typing import Any, cast, Callable, Union + +from flask import Blueprint, Response +from flask.views import MethodView + +import ckan.lib.base as base +import ckan.plugins.toolkit as tk + +from ckan import authz, logic, model +from ckan.logic import parse_params + +from ckanext.editable_config.shared import value_as_string + +from ckanext.collection.shared import get_collection + +from ckanext.ap_main.utils import ap_before_request +from ckanext.ap_main.views.generics import ApConfigurationPageView + +log = logging.getLogger(__name__) +doi_dashboard = Blueprint("doi_dashboard", __name__, url_prefix="/admin-panel/doi") +doi_dashboard.before_request(ap_before_request) + + +class ApConfigurationDisplayPageView(MethodView): + def get(self): + self.schema = tk.h.ap_get_arbitrary_schema("ap_doi_config") + data = self.get_config_form_data() + + return tk.render( + "ap_example/display_config.html", + extra_vars={"schema": self.schema, "data": data}, + ) + + def get_config_form_data(self) -> dict[str, Any]: + """Fetch/humanize configuration values from a CKANConfig""" + + data = {} + + for field in self.schema["fields"]: + if field["field_name"] not in tk.config: + continue + + data[field["field_name"]] = value_as_string( + field["field_name"], tk.config[field["field_name"]] + ) + + return data + + +class ApDoiView(MethodView): + def get(self) -> Union[str, Response]: + + return tk.render( + "ap_doi/list.html", + extra_vars={ + "collection": get_collection("ap-doi", parse_params(tk.request.args)), + }, + ) + + def post(self) -> Response: + bulk_action = tk.request.form.get("bulk-action") + package_ids = tk.request.form.getlist("entity_id") + + action_func = self._get_bulk_action(bulk_action) if bulk_action else None + + if not action_func: + tk.h.flash_error(tk._("The bulk action is not implemented")) + return tk.redirect_to("doi_dashboard.list") + + for package_id in package_ids: + try: + action_func(package_id) + except tk.ValidationError as e: + tk.h.flash_error(str(e)) + + return tk.redirect_to("doi_dashboard.list") + + def _get_bulk_action(self, value: str) -> Callable[[str], None] | None: + return { + "1": self._remove_file, + }.get(value) + + def _remove_file(self, package_id: str) -> None: + create_or_update_doi(package_id) + + +@doi_dashboard.before_request +def before_request(): + try: + tk.check_access( + "sysadmin", + { + "model": model, + "user": tk.g.user, + "auth_user_obj": tk.g.userobj, + }, + ) + except tk.NotAuthorized: + base.abort(403, tk._("Need to be system administrator to administer")) + + +def create_or_update_doi(package_id: str): + try: + result = tk.get_action("ap_doi_update_doi")({}, {"package_id": package_id}) + if result["status"] == "error": + for err in result["errors"]: + tk.h.flash_error(err) + else: + tk.h.flash_success(result["message"]) + except Exception: + pass + + return tk.h.redirect_to("doi_dashboard.list") + + +doi_dashboard.add_url_rule("/update_doi/", view_func=create_or_update_doi) +doi_dashboard.add_url_rule("/list", view_func=ApDoiView.as_view("list")) +doi_dashboard.add_url_rule( + "/config", + view_func=ApConfigurationPageView.as_view("config", "ap_doi_config"), +) diff --git a/ckanext/ap_example/plugin.py b/ckanext/ap_example/plugin.py index abd1a6a..da3ce7e 100644 --- a/ckanext/ap_example/plugin.py +++ b/ckanext/ap_example/plugin.py @@ -1,5 +1,4 @@ from __future__ import annotations -from typing import Literal from os import path @@ -19,6 +18,7 @@ class AdminPanelExamplePlugin(p.SingletonPlugin): p.implements(p.IConfigurer) p.implements(p.IConfigDeclaration, inherit=True) p.implements(p.ISignal) + p.implements(p.IConfigDeclaration) # IConfigurer diff --git a/ckanext/ap_example/views.py b/ckanext/ap_example/views.py index 7fb5e04..82e7f0e 100644 --- a/ckanext/ap_example/views.py +++ b/ckanext/ap_example/views.py @@ -8,6 +8,8 @@ import ckan.plugins.toolkit as tk +from ckanext.editable_config.shared import value_as_string + from ckanext.ap_main.utils import ap_before_request from ckanext.ap_main.views.generics import ApConfigurationPageView @@ -29,7 +31,6 @@ def get(self): def get_config_form_data(self) -> dict[str, Any]: """Fetch/humanize configuration values from a CKANConfig""" - from ckanext.editable_config.shared import value_as_string data = {} diff --git a/ckanext/ap_main/assets/js/vendor/htmx.min.js b/ckanext/ap_main/assets/js/vendor/htmx.min.js index cc0d236..010f584 100644 --- a/ckanext/ap_main/assets/js/vendor/htmx.min.js +++ b/ckanext/ap_main/assets/js/vendor/htmx.min.js @@ -1 +1,2 @@ -(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var G={onLoad:t,process:Nt,on:ue,off:fe,trigger:oe,ajax:mr,find:b,findAll:f,closest:d,values:function(e,t){var r=Qt(e,t||"post");return r.values},remove:B,addClass:V,removeClass:n,toggleClass:j,takeClass:W,defineExtension:Er,removeExtension:Cr,logAll:F,logNone:U,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"]},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=G.config.wsBinaryType;return t},version:"1.9.3"};var C={addTriggerHandler:bt,bodyContains:re,canAccessLocalStorage:D,findThisElement:de,filterValues:ir,hasAttribute:q,getAttributeValue:Z,getClosestAttributeValue:Y,getClosestMatch:c,getExpressionVars:vr,getHeaders:nr,getInputValues:Qt,getInternalData:ee,getSwapSpecification:or,getTriggerSpecs:Ge,getTarget:ve,makeFragment:l,mergeObjects:ne,makeSettleInfo:S,oobSwap:xe,querySelectorExt:ie,selectAndSwap:Xe,settleImmediately:Wt,shouldCancel:Qe,triggerEvent:oe,triggerErrorEvent:ae,withExtensions:w};var R=["get","post","put","delete","patch"];var O=R.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function J(e,t){return e.getAttribute&&e.getAttribute(t)}function q(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function Z(e,t){return J(e,t)||J(e,"data-"+t)}function u(e){return e.parentElement}function K(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function T(e,t,r){var n=Z(t,r);var i=Z(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function Y(t,r){var n=null;c(t,function(e){return n=T(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function H(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=K().createDocumentFragment()}return i}function L(e){return e.match(/",0);return r.querySelector("template").content}else{var n=H(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i(""+e+"
    ",1);case"col":return i(""+e+"
    ",2);case"tr":return i(""+e+"
    ",2);case"td":case"th":return i(""+e+"
    ",3);case"script":return i("
    "+e+"
    ",1);default:return i(e,0)}}}function Q(e){if(e){e()}}function A(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function N(e){return A(e,"Function")}function I(e){return A(e,"Object")}function ee(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function k(e){var t=[];if(e){for(var r=0;r=0}function re(e){if(e.getRootNode&&e.getRootNode()instanceof ShadowRoot){return K().body.contains(e.getRootNode().host)}else{return K().body.contains(e)}}function M(e){return e.trim().split(/\s+/)}function ne(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function y(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function D(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function X(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return cr(K().body,function(){return eval(e)})}function t(t){var e=G.on("htmx:load",function(e){t(e.detail.elt)});return e}function F(){G.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){G.logger=null}function b(e,t){if(t){return e.querySelector(t)}else{return b(K(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(K(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function V(e,t,r){e=s(e);if(r){setTimeout(function(){V(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function j(e,t){e=s(e);e.classList.toggle(t)}function W(e,t){e=s(e);te(e.parentElement.children,function(e){n(e,t)});V(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function r(e){var t=e.trim();if(t.startsWith("<")&&t.endsWith("/>")){return t.substring(1,t.length-2)}else{return t}}function _(e,t){if(t.indexOf("closest ")===0){return[d(e,r(t.substr(8)))]}else if(t.indexOf("find ")===0){return[b(e,r(t.substr(5)))]}else if(t.indexOf("next ")===0){return[z(e,r(t.substr(5)))]}else if(t.indexOf("previous ")===0){return[$(e,r(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else{return K().querySelectorAll(r(t))}}var z=function(e,t){var r=K().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ie(e,t){if(t){return _(e,t)[0]}else{return _(K().body,e)[0]}}function s(e){if(A(e,"String")){return b(e)}else{return e}}function le(e,t,r){if(N(t)){return{target:K().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function ue(t,r,n){Or(function(){var e=le(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=N(r);return e?r:n}function fe(t,r,n){Or(function(){var e=le(t,r,n);e.target.removeEventListener(e.event,e.listener)});return N(r)?r:n}var ce=K().createElement("output");function he(e,t){var r=Y(e,t);if(r){if(r==="this"){return[de(e,t)]}else{var n=_(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[ce]}else{return n}}}}function de(e,t){return c(e,function(e){return Z(e,t)!=null})}function ve(e){var t=Y(e,"hx-target");if(t){if(t==="this"){return de(e,"hx-target")}else{return ie(e,t)}}else{var r=ee(e);if(r.boosted){return K().body}else{return e}}}function ge(e){var t=G.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=K().querySelectorAll(t);if(r){te(r,function(e){var t;var r=i.cloneNode(true);t=K().createDocumentFragment();t.appendChild(r);if(!me(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!oe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Me(o,e,e,t,a)}te(a.elts,function(e){oe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ae(K().body,"htmx:oobErrorNoTarget",{content:i})}return e}function ye(e,t,r){var n=Y(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var t=e.id.replace("'","\\'");var r=e.tagName.replace(":","\\:");var n=a.querySelector(r+"[id='"+t+"']");if(n&&n!==a){var i=e.cloneNode();pe(e,n);o.tasks.push(function(){pe(e,i)})}}})}function Se(e){return function(){n(e,G.config.addedClass);Nt(e);St(e);Ee(e);oe(e,"htmx:load")}}function Ee(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){we(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;V(i,G.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Se(i))}}}function Ce(e,t){var r=0;while(re!=t);while(n&&n!==t){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}o(t);u(t).removeChild(t)}}function He(e,t,r){return a(e,e.firstChild,t,r)}function Le(e,t,r){return a(u(e),e,t,r)}function Ae(e,t,r){return a(e,null,t,r)}function Ne(e,t,r){return a(u(e),e.nextSibling,t,r)}function Ie(e,t,r){o(e);return u(e).removeChild(e)}function ke(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){o(n.nextSibling);e.removeChild(n.nextSibling)}o(n);e.removeChild(n)}}function Pe(e,t,r){var n=r||Y(e,"hx-select");if(n){var i=K().createDocumentFragment();te(t.querySelectorAll(n),function(e){i.appendChild(e)});t=i}return t}function Me(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":Te(r,n,i);return;case"afterbegin":He(r,n,i);return;case"beforebegin":Le(r,n,i);return;case"beforeend":Ae(r,n,i);return;case"afterend":Ne(r,n,i);return;case"delete":Ie(r,n,i);return;default:var a=Rr(t);for(var o=0;o-1){var t=e.replace(/]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Xe(e,t,r,n,i,a){i.title=De(n);var o=l(n);if(o){ye(r,o,i);o=Pe(r,o,a);be(o);return Me(e,r,t,o,i)}}function Fe(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=y(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!I(o)){o={value:o}}oe(r,a,o)}}}else{oe(r,n,[])}}var Ue=/\s/;var g=/[\s,]/;var Be=/[_$a-zA-Z]/;var Ve=/[_$a-zA-Z0-9]/;var je=['"',"'","/"];var p=/[^\s]/;function We(e){var t=[];var r=0;while(r0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=cr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ae(K().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(_e(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function m(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var $e="input, textarea, select";function Ge(e){var t=Z(e,"hx-trigger");var r=[];if(t){var n=We(t);do{m(n,p);var i=n.length;var a=m(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};m(n,p);o.pollInterval=v(m(n,/[,\[\s]/));m(n,p);var s=ze(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=ze(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){m(n,p);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(m(n,g))}else if(u==="from"&&n[0]===":"){n.shift();var f=m(n,g);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();f+=" "+m(n,g)}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=m(n,g)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(m(n,g))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=m(n,g)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=m(n,g)}else{ae(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ae(e,"htmx:syntax:error",{token:n.shift()})}m(n,p)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"]')){return[{trigger:"click"}]}else if(h(e,$e)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Je(e){ee(e).cancelled=true}function Ze(e,t,r){var n=ee(e);n.timeout=setTimeout(function(){if(re(e)&&n.cancelled!==true){if(!tt(r,e,kt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ze(e,t,r)}},r.pollInterval)}function Ke(e){return location.hostname===e.hostname&&J(e,"href")&&J(e,"href").indexOf("#")!==0}function Ye(t,r,e){if(t.tagName==="A"&&Ke(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=t.href}else{var a=J(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=J(t,"action")}e.forEach(function(e){rt(t,function(e,t){se(n,i,e,t)},r,e,true)})}}function Qe(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function et(e,t){return ee(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function tt(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){ae(K().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function rt(i,a,e,o,s){var l=ee(i);var t;if(o.from){t=_(i,o.from)}else{t=[i]}if(o.changed){l.lastValue=i.value}te(t,function(r){var n=function(e){if(!re(i)){r.removeEventListener(o.trigger,n);return}if(et(i,e)){return}if(s||Qe(e,i)){e.preventDefault()}if(tt(o,i,e)){return}var t=ee(e);t.triggerSpec=o;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(i)<0){t.handledFor.push(i);if(o.consume){e.stopPropagation()}if(o.target&&e.target){if(!h(e.target,o.target)){return}}if(o.once){if(l.triggeredOnce){return}else{l.triggeredOnce=true}}if(o.changed){if(l.lastValue===i.value){return}else{l.lastValue=i.value}}if(l.delayed){clearTimeout(l.delayed)}if(l.throttle){return}if(o.throttle){if(!l.throttle){a(i,e);l.throttle=setTimeout(function(){l.throttle=null},o.throttle)}}else if(o.delay){l.delayed=setTimeout(function(){a(i,e)},o.delay)}else{oe(i,"htmx:trigger");a(i,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:o.trigger,listener:n,on:r});r.addEventListener(o.trigger,n)})}var nt=false;var it=null;function at(){if(!it){it=function(){nt=true};window.addEventListener("scroll",it);setInterval(function(){if(nt){nt=false;te(K().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){ot(e)})}},200)}}function ot(t){if(!q(t,"data-hx-revealed")&&P(t)){t.setAttribute("data-hx-revealed","true");var e=ee(t);if(e.initHash){oe(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){oe(t,"revealed")},{once:true})}}}function st(e,t,r){var n=M(r);for(var i=0;i=0){var t=ct(n);setTimeout(function(){lt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ee(s).webSocket=t;t.addEventListener("message",function(e){if(ut(s)){return}var t=e.data;w(s,function(e){t=e.transformResponse(t,null,s)});var r=S(s);var n=l(t);var i=k(n.children);for(var a=0;a0){oe(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(Qe(e,u)){e.preventDefault()}})}else{ae(u,"htmx:noWebSocketSourceError")}}function ct(e){var t=G.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}x('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function ht(e,t,r){var n=M(r);for(var i=0;i0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=qt(o)}for(var l in r){Tt(e,l,r[l])}}}function Lt(t){Oe(t);for(const e of t.attributes){const{name:r,value:n}=e;if(r.startsWith("hx-on:")||r.startsWith("data-hx-on:")){let e=r.slice(r.indexOf(":")+1);if(e.startsWith(":"))e="htmx"+e;Tt(t,e,n)}}}function At(t){if(t.closest&&t.closest(G.config.disableSelector)){return}var r=ee(t);if(r.initHash!==Re(t)){r.initHash=Re(t);qe(t);Ht(t);oe(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=Ge(t);var n=yt(t,r,e);if(!n){if(Y(t,"hx-boost")==="true"){Ye(t,r,e)}else if(q(t,"hx-trigger")){e.forEach(function(e){bt(t,e,r,function(){})})}}if(t.tagName==="FORM"){Ot(t)}var i=Z(t,"hx-sse");if(i){ht(t,r,i)}var a=Z(t,"hx-ws");if(a){st(t,r,a)}oe(t,"htmx:afterProcessNode")}}function Nt(e){e=s(e);At(e);te(Rt(e),function(e){At(e)});te(Ct(e),Lt)}function It(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function kt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=K().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function ae(e,t,r){oe(e,t,ne({error:t},r))}function Pt(e){return e==="htmx:afterProcessNode"}function w(e,t){te(Rr(e),function(e){try{t(e)}catch(e){x(e)}})}function x(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function oe(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=kt(t,r);if(G.logger&&!Pt(t)){G.logger(e,t,r)}if(r.error){x(r.error);oe(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=It(t);if(i&&a!==t){var o=kt(a,n.detail);i=i&&e.dispatchEvent(o)}w(e,function(e){i=i&&e.onEvent(t,n)!==false});return i}var Mt=location.pathname+location.search;function Dt(){var e=K().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||K().body}function Xt(e,t,r,n){if(!D()){return}e=X(e);var i=y(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;aG.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ae(K().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Ft(e){if(!D()){return null}e=X(e);var t=y(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){oe(K().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Dt();var r=S(t);var n=De(this.response);if(n){var i=b("title");if(i){i.innerHTML=n}else{window.document.title=n}}ke(t,e,r);Wt(r.tasks);Mt=a;oe(K().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ae(K().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function zt(e){Bt();e=e||location.pathname+location.search;var t=Ft(e);if(t){var r=l(t.content);var n=Dt();var i=S(n);ke(n,r,i);Wt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Mt=e;oe(K().body,"htmx:historyRestore",{path:e,item:t})}else{if(G.config.refreshOnHistoryMiss){window.location.reload(true)}else{_t(e)}}}function $t(e){var t=he(e,"hx-indicator");if(t==null){t=[e]}te(t,function(e){var t=ee(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,G.config.requestClass)});return t}function Gt(e){te(e,function(e){var t=ee(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,G.config.requestClass)}})}function Jt(e,t){for(var r=0;r=0}function or(e,t){var r=t?t:Y(e,"hx-swap");var n={swapStyle:ee(e).boosted?"innerHTML":G.config.defaultSwapStyle,swapDelay:G.config.defaultSwapDelay,settleDelay:G.config.defaultSettleDelay};if(ee(e).boosted&&!ar(e)){n["show"]="top"}if(r){var i=M(r);if(i.length>0){n["swapStyle"]=i[0];for(var a=1;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}}}}return n}function sr(e){return Y(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&J(e,"enctype")==="multipart/form-data"}function lr(t,r,n){var i=null;w(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(sr(r)){return rr(n)}else{return tr(n)}}}function S(e){return{tasks:[],elts:[e]}}function ur(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ie(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ie(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:G.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:G.config.scrollBehavior})}}}function fr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=Z(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=cr(e,function(){return Function("return ("+a+")")()},{})}else{s=y(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return fr(u(e),t,r,n)}function cr(e,t,r){if(G.config.allowEval){return t()}else{ae(e,"htmx:evalDisallowedError");return r}}function hr(e,t){return fr(e,"hx-vars",true,t)}function dr(e,t){return fr(e,"hx-vals",false,t)}function vr(e){return ne(hr(e),dr(e))}function gr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function pr(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ae(K().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function E(e,t){return e.getAllResponseHeaders().match(t)}function mr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||A(r,"String")){return se(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return se(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return se(e,t,null,null,{returnPromise:true})}}function xr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function se(e,t,n,r,i,M){var a=null;var o=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var s=new Promise(function(e,t){a=e;o=t})}if(n==null){n=K().body}var D=i.handler||br;if(!re(n)){return}var l=i.targetOverride||ve(n);if(l==null||l==ce){ae(n,"htmx:targetError",{target:Z(n,"hx-target")});return}if(!M){var X=function(){return se(e,t,n,r,i,true)};var F={target:l,elt:n,path:t,verb:e,triggeringEvent:r,etc:i,issueRequest:X};if(oe(n,"htmx:confirm",F)===false){return}}var u=n;var f=ee(n);var c=Y(n,"hx-sync");var h=null;var d=false;if(c){var v=c.split(":");var g=v[0].trim();if(g==="this"){u=de(n,"hx-sync")}else{u=ie(n,g)}c=(v[1]||"drop").trim();f=ee(u);if(c==="drop"&&f.xhr&&f.abortable!==true){return}else if(c==="abort"){if(f.xhr){return}else{d=true}}else if(c==="replace"){oe(u,"htmx:abort")}else if(c.indexOf("queue")===0){var U=c.split(" ");h=(U[1]||"last").trim()}}if(f.xhr){if(f.abortable){oe(u,"htmx:abort")}else{if(h==null){if(r){var p=ee(r);if(p&&p.triggerSpec&&p.triggerSpec.queue){h=p.triggerSpec.queue}}if(h==null){h="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(h==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){se(e,t,n,r,i)})}else if(h==="all"){f.queuedRequests.push(function(){se(e,t,n,r,i)})}else if(h==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){se(e,t,n,r,i)})}return}}var m=new XMLHttpRequest;f.xhr=m;f.abortable=d;var x=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var y=Y(n,"hx-prompt");if(y){var b=prompt(y);if(b===null||!oe(n,"htmx:prompt",{prompt:b,target:l})){Q(a);x();return s}}var w=Y(n,"hx-confirm");if(w){if(!confirm(w)){Q(a);x();return s}}var S=nr(n,l,b);if(i.headers){S=ne(S,i.headers)}var E=Qt(n,e);var C=E.errors;var R=E.values;if(i.values){R=ne(R,i.values)}var B=vr(n);var O=ne(R,B);var q=ir(O,n);if(e!=="get"&&!sr(n)){S["Content-Type"]="application/x-www-form-urlencoded"}if(G.config.getCacheBusterParam&&e==="get"){q["org.htmx.cache-buster"]=J(l,"id")||"true"}if(t==null||t===""){t=K().location.href}var T=fr(n,"hx-request");var V=ee(n).boosted;var H=G.config.methodsThatUseUrlParams.indexOf(e)>=0;var L={boosted:V,useUrlParams:H,parameters:q,unfilteredParameters:O,headers:S,target:l,verb:e,errors:C,withCredentials:i.credentials||T.credentials||G.config.withCredentials,timeout:i.timeout||T.timeout||G.config.timeout,path:t,triggeringEvent:r};if(!oe(n,"htmx:configRequest",L)){Q(a);x();return s}t=L.path;e=L.verb;S=L.headers;q=L.parameters;C=L.errors;H=L.useUrlParams;if(C&&C.length>0){oe(n,"htmx:validation:halted",L);Q(a);x();return s}var j=t.split("#");var W=j[0];var A=j[1];var N=t;if(H){N=W;var _=Object.keys(q).length!==0;if(_){if(N.indexOf("?")<0){N+="?"}else{N+="&"}N+=tr(q);if(A){N+="#"+A}}}m.open(e.toUpperCase(),N,true);m.overrideMimeType("text/html");m.withCredentials=L.withCredentials;m.timeout=L.timeout;if(T.noHeaders){}else{for(var I in S){if(S.hasOwnProperty(I)){var z=S[I];gr(m,I,z)}}}var k={xhr:m,target:l,requestConfig:L,etc:i,boosted:V,pathInfo:{requestPath:t,finalRequestPath:N,anchor:A}};m.onload=function(){try{var e=xr(n);k.pathInfo.responsePath=pr(m);D(n,k);Gt(P);oe(n,"htmx:afterRequest",k);oe(n,"htmx:afterOnLoad",k);if(!re(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(re(r)){t=r}}if(t){oe(t,"htmx:afterRequest",k);oe(t,"htmx:afterOnLoad",k)}}Q(a);x()}catch(e){ae(n,"htmx:onLoadError",ne({error:e},k));throw e}};m.onerror=function(){Gt(P);ae(n,"htmx:afterRequest",k);ae(n,"htmx:sendError",k);Q(o);x()};m.onabort=function(){Gt(P);ae(n,"htmx:afterRequest",k);ae(n,"htmx:sendAbort",k);Q(o);x()};m.ontimeout=function(){Gt(P);ae(n,"htmx:afterRequest",k);ae(n,"htmx:timeout",k);Q(o);x()};if(!oe(n,"htmx:beforeRequest",k)){Q(a);x();return s}var P=$t(n);te(["loadstart","loadend","progress","abort"],function(t){te([m,m.upload],function(e){e.addEventListener(t,function(e){oe(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});oe(n,"htmx:beforeSend",k);var $=H?null:lr(m,n,q);m.send($);return s}function yr(e,t){var r=t.xhr;var n=null;var i=null;if(E(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(E(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(E(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=Y(e,"hx-push-url");var l=Y(e,"hx-replace-url");var u=ee(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function br(l,u){var f=u.xhr;var c=u.target;var e=u.etc;if(!oe(l,"htmx:beforeOnLoad",u))return;if(E(f,/HX-Trigger:/i)){Fe(f,"HX-Trigger",l)}if(E(f,/HX-Location:/i)){Bt();var t=f.getResponseHeader("HX-Location");var h;if(t.indexOf("{")===0){h=y(t);t=h["path"];delete h["path"]}mr("GET",t,h).then(function(){Vt(t)});return}if(E(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");return}if(E(f,/HX-Refresh:/i)){if("true"===f.getResponseHeader("HX-Refresh")){location.reload();return}}if(E(f,/HX-Retarget:/i)){u.target=K().querySelector(f.getResponseHeader("HX-Retarget"))}var d=yr(l,u);var r=f.status>=200&&f.status<400&&f.status!==204;var v=f.response;var n=f.status>=400;var i=ne({shouldSwap:r,serverResponse:v,isError:n},u);if(!oe(c,"htmx:beforeSwap",i))return;c=i.target;v=i.serverResponse;n=i.isError;u.target=c;u.failed=n;u.successful=!n;if(i.shouldSwap){if(f.status===286){Je(l)}w(l,function(e){v=e.transformResponse(v,f,l)});if(d.type){Bt()}var a=e.swapOverride;if(E(f,/HX-Reswap:/i)){a=f.getResponseHeader("HX-Reswap")}var h=or(l,a);c.classList.add(G.config.swappingClass);var g=null;var p=null;var o=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(E(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}var n=S(c);Xe(h.swapStyle,c,l,v,n,r);if(t.elt&&!re(t.elt)&&t.elt.id){var i=document.getElementById(t.elt.id);var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!G.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(G.config.swappingClass);te(n.elts,function(e){if(e.classList){e.classList.add(G.config.settlingClass)}oe(e,"htmx:afterSwap",u)});if(E(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!re(l)){o=K().body}Fe(f,"HX-Trigger-After-Swap",o)}var s=function(){te(n.tasks,function(e){e.call()});te(n.elts,function(e){if(e.classList){e.classList.remove(G.config.settlingClass)}oe(e,"htmx:afterSettle",u)});if(d.type){if(d.type==="push"){Vt(d.path);oe(K().body,"htmx:pushedIntoHistory",{path:d.path})}else{jt(d.path);oe(K().body,"htmx:replacedInHistory",{path:d.path})}}if(u.pathInfo.anchor){var e=b("#"+u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title){var t=b("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}ur(n.elts,h);if(E(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!re(l)){r=K().body}Fe(f,"HX-Trigger-After-Settle",r)}Q(g)};if(h.settleDelay>0){setTimeout(s,h.settleDelay)}else{s()}}catch(e){ae(l,"htmx:swapError",u);Q(p);throw e}};var s=G.config.globalViewTransitions;if(h.hasOwnProperty("transition")){s=h.transition}if(s&&oe(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var m=new Promise(function(e,t){g=e;p=t});var x=o;o=function(){document.startViewTransition(function(){x();return m})}}if(h.swapDelay>0){setTimeout(o,h.swapDelay)}else{o()}}if(n){ae(l,"htmx:responseError",ne({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var wr={};function Sr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Er(e,t){if(t.init){t.init(C)}wr[e]=ne(Sr(),t)}function Cr(e){delete wr[e]}function Rr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=Z(e,"hx-ext");if(t){te(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=wr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Rr(u(e),r,n)}function Or(e){if(K().readyState!=="loading"){e()}else{K().addEventListener("DOMContentLoaded",e)}}function qr(){if(G.config.includeIndicatorStyles!==false){K().head.insertAdjacentHTML("beforeend","")}}function Tr(){var e=K().querySelector('meta[name="htmx-config"]');if(e){return y(e.content)}else{return null}}function Hr(){var e=Tr();if(e){G.config=ne(G.config,e)}}Or(function(){Hr();qr();var e=K().body;Nt(e);var t=K().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ee(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){zt();te(t,function(e){oe(e,"htmx:restored",{document:K(),triggerEvent:oe})})}else{if(r){r(e)}}};setTimeout(function(){oe(e,"htmx:load",{});e=null},0)});return G}()}); +// [1.9.12] - 2024-04-17 +(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:F,process:zt,on:de,off:ge,trigger:ce,ajax:Nr,find:C,findAll:f,closest:v,values:function(e,t){var r=dr(e,t||"post");return r.values},remove:_,addClass:z,removeClass:n,toggleClass:$,takeClass:W,defineExtension:Ur,removeExtension:Br,logAll:V,logNone:j,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null},parseInterval:d,_:t,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.12"};var r={addTriggerHandler:Lt,bodyContains:se,canAccessLocalStorage:U,findThisElement:xe,filterValues:yr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Hr,getHeaders:xr,getInputValues:dr,getInternalData:ae,getSwapSpecification:wr,getTriggerSpecs:it,getTarget:ye,makeFragment:l,mergeObjects:le,makeSettleInfo:T,oobSwap:Ee,querySelectorExt:ue,selectAndSwap:je,settleImmediately:nr,shouldCancel:ut,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:R};var w=["get","post","put","delete","patch"];var i=w.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");var S=e("head"),q=e("title"),H=e("svg",true);function e(e,t){return new RegExp("<"+e+"(\\s[^>]*>|>)([\\s\\S]*?)<\\/"+e+">",!!t?"gim":"im")}function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function L(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=L(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function A(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function s(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function N(e){return/",0);var a=i.querySelector("template").content;if(Q.config.allowScriptTags){oe(a.querySelectorAll("script"),function(e){if(Q.config.inlineScriptNonce){e.nonce=Q.config.inlineScriptNonce}e.htmxExecuted=navigator.userAgent.indexOf("Firefox")===-1})}else{oe(a.querySelectorAll("script"),function(e){_(e)})}return a}switch(r){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return s(""+n+"
    ",1);case"col":return s(""+n+"
    ",2);case"tr":return s(""+n+"
    ",2);case"td":case"th":return s(""+n+"
    ",3);case"script":case"style":return s("
    "+n+"
    ",1);default:return s(n,0)}}function ie(e){if(e){e()}}function I(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function k(e){return I(e,"Function")}function P(e){return I(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function M(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function D(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){b(e);return null}}function U(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function B(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!/^\/$/.test(t)){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function t(e){return Tr(re().body,function(){return eval(e)})}function F(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function V(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function j(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function _(e,t){e=p(e);if(t){setTimeout(function(){_(e);e=null},t)}else{e.parentElement.removeChild(e)}}function z(e,t,r){e=p(e);if(r){setTimeout(function(){z(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=p(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function $(e,t){e=p(e);e.classList.toggle(t)}function W(e,t){e=p(e);oe(e.parentElement.children,function(e){n(e,t)});z(e,t)}function v(e,t){e=p(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function G(e,t){return e.substring(e.length-t.length)===t}function J(e){var t=e.trim();if(g(t,"<")&&G(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function Z(e,t){if(t.indexOf("closest ")===0){return[v(e,J(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,J(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[K(e,J(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[Y(e,J(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(J(t))}}var K=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return Z(e,t)[0]}else{return Z(re().body,e)[0]}}function p(e){if(I(e,"String")){return C(e)}else{return e}}function ve(e,t,r){if(k(t)){return{target:re().body,event:e,listener:t}}else{return{target:p(e),event:t,listener:r}}}function de(t,r,n){jr(function(){var e=ve(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=k(r);return e?r:n}function ge(t,r,n){jr(function(){var e=ve(t,r,n);e.target.removeEventListener(e.event,e.listener)});return k(r)?r:n}var pe=re().createElement("output");function me(e,t){var r=ne(e,t);if(r){if(r==="this"){return[xe(e,t)]}else{var n=Z(e,r);if(n.length===0){b('The selector "'+r+'" on '+t+" returned no matches!");return[pe]}else{return n}}}}function xe(e,t){return c(e,function(e){return te(e,t)!=null})}function ye(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return xe(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function be(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!Se(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){Fe(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function Ce(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(var a=0;a0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();we(e,i);s.tasks.push(function(){we(e,a)})}}})}function Oe(e){return function(){n(e,Q.config.addedClass);zt(e);Nt(e);qe(e);ce(e,"htmx:load")}}function qe(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Te(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;z(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Oe(i))}}}function He(e,t){var r=0;while(r-1){var t=e.replace(H,"");var r=t.match(q);if(r){return r[2]}}}function je(e,t,r,n,i,a){i.title=Ve(n);var o=l(n);if(o){Ce(r,o,i);o=Be(r,o,a);Re(o);return Fe(e,r,t,o,i)}}function _e(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!P(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=Tr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Qe(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!t.test(e[0])){r+=e.shift()}return r}function tt(e){var t;if(e.length>0&&Ze.test(e[0])){e.shift();t=y(e,Ke).trim();e.shift()}else{t=y(e,x)}return t}var rt="input, textarea, select";function nt(e,t,r){var n=[];var i=Ye(t);do{y(i,Je);var a=i.length;var o=y(i,/[,\[\s]/);if(o!==""){if(o==="every"){var s={trigger:"every"};y(i,Je);s.pollInterval=d(y(i,/[,\[\s]/));y(i,Je);var l=et(e,i,"event");if(l){s.eventFilter=l}n.push(s)}else if(o.indexOf("sse:")===0){n.push({trigger:"sse",sseEvent:o.substr(4)})}else{var u={trigger:o};var l=et(e,i,"event");if(l){u.eventFilter=l}while(i.length>0&&i[0]!==","){y(i,Je);var f=i.shift();if(f==="changed"){u.changed=true}else if(f==="once"){u.once=true}else if(f==="consume"){u.consume=true}else if(f==="delay"&&i[0]===":"){i.shift();u.delay=d(y(i,x))}else if(f==="from"&&i[0]===":"){i.shift();if(Ze.test(i[0])){var c=tt(i)}else{var c=y(i,x);if(c==="closest"||c==="find"||c==="next"||c==="previous"){i.shift();var h=tt(i);if(h.length>0){c+=" "+h}}}u.from=c}else if(f==="target"&&i[0]===":"){i.shift();u.target=tt(i)}else if(f==="throttle"&&i[0]===":"){i.shift();u.throttle=d(y(i,x))}else if(f==="queue"&&i[0]===":"){i.shift();u.queue=y(i,x)}else if(f==="root"&&i[0]===":"){i.shift();u[f]=tt(i)}else if(f==="threshold"&&i[0]===":"){i.shift();u[f]=y(i,x)}else{fe(e,"htmx:syntax:error",{token:i.shift()})}}n.push(u)}}if(i.length===a){fe(e,"htmx:syntax:error",{token:i.shift()})}y(i,Je)}while(i[0]===","&&i.shift());if(r){r[t]=n}return n}function it(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Q.config.triggerSpecsCache;r=n&&n[t]||nt(e,t,n)}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,rt)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function at(e){ae(e).cancelled=true}function ot(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ct(r,e,Wt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}ot(e,t,r)}},r.pollInterval)}function st(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function lt(t,r,e){if(t.tagName==="A"&&st(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){ht(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function ut(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function ft(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ct(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function ht(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=Z(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(ft(a,e)){return}if(l||ut(e,a)){e.preventDefault()}if(ct(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle>0){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay>0){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var vt=false;var dt=null;function gt(){if(!dt){dt=function(){vt=true};window.addEventListener("scroll",dt);setInterval(function(){if(vt){vt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){pt(e)})}},200)}}function pt(t){if(!o(t,"data-hx-revealed")&&X(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function mt(e,t,r){var n=D(r);for(var i=0;i=0){var t=wt(n);setTimeout(function(){xt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(yt(s)){return}var t=e.data;R(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=M(n.children);for(var a=0;a0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(ut(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function wt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}b('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function St(e,t,r){var n=D(r);for(var i=0;i0){setTimeout(i,n)}else{i()}}function Ht(t,i,e){var a=false;oe(w,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Lt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Lt(n,e,t,r){if(e.sseEvent){Rt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){gt();ht(n,r,t,e);pt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t0){t.polling=true;ot(n,r,e)}else{ht(n,r,t,e)}}function At(e){if(!e.htmxExecuted&&Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){b(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Nt(e){if(h(e,"script")){At(e)}oe(f(e,"script"),function(e){At(e)})}function It(e){var t=e.attributes;if(!t){return false}for(var r=0;r0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Bt(o)}for(var l in r){Ft(e,l,r[l])}}}function jt(e){Ae(e);for(var t=0;tQ.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Yt(e){if(!U()){return null}e=B(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Zt();var r=T(t);var n=Ve(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Ue(t,e,r);nr(r.tasks);Jt=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function ar(e){er();e=e||location.pathname+location.search;var t=Yt(e);if(t){var r=l(t.content);var n=Zt();var i=T(n);Ue(n,r,i);nr(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Jt=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{ir(e)}}}function or(e){var t=me(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function sr(e){var t=me(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function lr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ur(e,t){for(var r=0;r=0}function wr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!br(e)){n["show"]="top"}if(r){var i=D(r);if(i.length>0){for(var a=0;a0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{b("Unknown modifier in hx-swap: "+o)}}}}return n}function Sr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function Er(t,r,n){var i=null;R(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(Sr(r)){return mr(n)}else{return pr(n)}}}function T(e){return{tasks:[],elts:[e]}}function Cr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Rr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=Tr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return Rr(u(e),t,r,n)}function Tr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Or(e,t){return Rr(e,"hx-vars",true,t)}function qr(e,t){return Rr(e,"hx-vals",false,t)}function Hr(e){return le(Or(e),qr(e))}function Lr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Ar(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return t.test(e.getAllResponseHeaders())}function Nr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||I(r,"String")){return he(e,t,null,null,{targetOverride:p(r),returnPromise:true})}else{return he(e,t,p(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:p(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function Ir(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function kr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Mr;var X=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ye(n);if(u==null||u==pe){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var D=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var p=ne(n,"hx-sync");var m=null;var x=false;if(p){var B=p.split(":");var F=B[0].trim();if(F==="this"){g=xe(n,"hx-sync")}else{g=ue(n,F)}p=(B[1]||"drop").trim();f=ae(g);if(p==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(p==="abort"){if(f.xhr){ie(o);return l}else{x=true}}else if(p==="replace"){ce(g,"htmx:abort")}else if(p.indexOf("queue")===0){var V=p.split(" ");m=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(m==null){if(i){var y=ae(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){m=y.triggerSpec.queue}}if(m==null){m="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(m==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(m==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=xr(n,u,S);if(t!=="get"&&!Sr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=dr(n,t);var C=_.errors;var R=_.values;if(a.values){R=le(R,a.values)}var z=Hr(n);var $=le(R,z);var T=yr($,n);if(Q.config.getCacheBusterParam&&t==="get"){T["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=Rr(n,"hx-request");var W=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:T,unfilteredParameters:$,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;T=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(T).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=pr(T);if(L){A+="#"+L}}}if(!kr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Lr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,select:X,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Ir(n);I.pathInfo.responsePath=Ar(b);M(n,I);lr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){lr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=or(n);var P=sr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:Er(b,n,T);b.send(Y);return l}function Pr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Mr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){_e(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){er();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Nr("GET",r,v).then(function(){tr(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){if(f.getResponseHeader("HX-Retarget")==="this"){u.target=l}else{u.target=ue(l,f.getResponseHeader("HX-Retarget"))}}var d=Pr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var p=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:p},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;p=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){at(l)}R(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){er()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=wr(l,s);if(v.hasOwnProperty("ignoreTitle")){p=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var m=null;var x=null;var y=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){tr(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{rr(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=T(c);je(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}_e(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!p){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}Cr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}_e(f,"HX-Trigger-After-Settle",r)}ie(m)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(x);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){m=e;x=t});var S=y;y=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(y,v.swapDelay)}else{y()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Xr={};function Dr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ur(e,t){if(t.init){t.init(r)}Xr[e]=le(Dr(),t)}function Br(e){delete Xr[e]}function Fr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Xr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Fr(u(e),r,n)}var Vr=false;re().addEventListener("DOMContentLoaded",function(){Vr=true});function jr(e){if(Vr||re().readyState==="complete"){e()}else{re().addEventListener("DOMContentLoaded",e)}}function _r(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","")}}function zr(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function $r(){var e=zr();if(e){Q.config=le(Q.config,e)}}jr(function(){$r();_r();var e=re().body;zt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});const r=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){ar();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); diff --git a/ckanext/ap_main/templates/admin_panel/panel_links.html b/ckanext/ap_main/templates/admin_panel/panel_links.html index aea3375..35acfb8 100644 --- a/ckanext/ap_main/templates/admin_panel/panel_links.html +++ b/ckanext/ap_main/templates/admin_panel/panel_links.html @@ -37,7 +37,5 @@ {% endmacro %} {% for button in h.ap_get_toolbar_structure() %} - {% if button.url or button.sibitems %} - {{ toolbar_button(button) }} - {% endif %} + {{ toolbar_button(button) }} {% endfor %} diff --git a/ckanext/ap_main/views/generics.py b/ckanext/ap_main/views/generics.py index 5dc200c..ea2dfa6 100644 --- a/ckanext/ap_main/views/generics.py +++ b/ckanext/ap_main/views/generics.py @@ -16,7 +16,7 @@ def __init__( breadcrum_label: str = "Configuration", page_title: str = "Configuration page", success_update_message: str = "Config options have been updated", - fields: list[dict[str, Any]] = None, + fields: list[dict[str, Any]] | None = None, ): """A generic view to render configurations for an extension based on an arbitrary schema and config_declaration.yaml diff --git a/setup.cfg b/setup.cfg index 719cea3..6abc91b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,11 +23,12 @@ namespace_packages = ckanext install_requires = typing-extensions ckanext-toolbelt - ckanext-collection>=0.1.0,<0.3.0 + ckanext-collection>=0.1.2,<0.3.0 ckanext-files>=0.1.0,<1.0.0 croniter>=2.0.1,<3.0.0 cron-descriptor>=1.4.0,<2.0.0 ckanext-wysiwyg>=0.1.0,<1.0.0 + ckanext-doi>=3.1.12,<4.0.0 include_package_data = True @@ -39,6 +40,8 @@ ckan.plugins = admin_panel_cron = ckanext.ap_cron.plugin:AdminPanelCronPlugin admin_panel_support = ckanext.ap_support.plugin:AdminPanelSupportPlugin admin_panel_example = ckanext.ap_example.plugin:AdminPanelExamplePlugin + admin_panel_doi = ckanext.ap_doi.plugin:AdminPanelDoiPlugin + ap_doi = ckanext.ap_doi.plugin:ApDOIPlugin ap_cron_test = ckanext.ap_cron.tests.factories:ApCronTestPlugin babel.extractors =