From 778665549f624b120ed58bd66fc3e92a44ef48ca Mon Sep 17 00:00:00 2001 From: mutantsan Date: Mon, 29 Jan 2024 16:00:22 +0200 Subject: [PATCH] feature: implement file manager, part 2 --- ckanext/file_manager/assets/js/fm-htmx.js | 59 +++++++++++++++++++ ckanext/file_manager/assets/webassets.yml | 9 +++ ckanext/file_manager/col_renderers.py | 29 +++++++++ ckanext/file_manager/collection.py | 50 ++++++++-------- ckanext/file_manager/plugin.py | 5 ++ .../templates/file_manager/list.html | 14 +++-- .../templates/file_manager/record.html | 8 ++- ckanext/file_manager/views.py | 14 +---- ckanext/files/helpers.py | 5 ++ ckanext/files/model/file.py | 6 +- ckanext/files/plugin.py | 1 + 11 files changed, 151 insertions(+), 49 deletions(-) create mode 100644 ckanext/file_manager/assets/js/fm-htmx.js create mode 100644 ckanext/file_manager/assets/webassets.yml create mode 100644 ckanext/file_manager/col_renderers.py create mode 100644 ckanext/files/helpers.py diff --git a/ckanext/file_manager/assets/js/fm-htmx.js b/ckanext/file_manager/assets/js/fm-htmx.js new file mode 100644 index 0000000..0159a45 --- /dev/null +++ b/ckanext/file_manager/assets/js/fm-htmx.js @@ -0,0 +1,59 @@ +ckan.module("fm-htmx", function ($) { + return { + options: { + formId: null, + }, + initialize: function () { + $.proxyAll(this, /_on/); + + document.addEventListener('htmx:beforeRequest', this._onHTMXbeforeRequest); + document.addEventListener('htmx:afterSettle', this._onHTMXafterSettle); + document.addEventListener('htmx:confirm', this._onHTMXconfirm); + document.addEventListener('htmx:afterRequest', this._onAfterRequest) + }, + + _onHTMXbeforeRequest: function (e) { + $(e.detail.target).find("[data-module]").unbind() + + for (const [key, _] of Object.entries(ckan.module.instances)) { + ckan.module.instances[key] = null; + } + }, + + _onHTMXafterSettle: function (e) { + const doNotInitialize = ["ap-hyperscript"] + + $(e.detail.target).find("[data-module]").each(function (_, element) { + const moduleName = $(element).attr("data-module"); + + if (!doNotInitialize.includes(moduleName)) { + ckan.module.initializeElement(element); + } + }) + }, + + _onHTMXconfirm: function (evt) { + if (evt.detail.path.includes("/file_manager/delete")) { + evt.preventDefault(); + + swal({ + text: this._("Are you sure you wish to delete a file?"), + icon: "warning", + buttons: true, + dangerMode: true, + }).then((confirmed) => { + if (confirmed) { + evt.detail.issueRequest(); + this.sandbox.publish("ap:notify", this._("A file has been removed")); + } + }); + } + }, + + _onAfterRequest: function (evt) { + if (evt.detail.pathInfo.requestPath.includes("/file_manager/delete/")) { + htmx.trigger(`#${this.options.formId}`, "change"); + } + } + }; +}); diff --git a/ckanext/file_manager/assets/webassets.yml b/ckanext/file_manager/assets/webassets.yml new file mode 100644 index 0000000..764c9fe --- /dev/null +++ b/ckanext/file_manager/assets/webassets.yml @@ -0,0 +1,9 @@ +file-manager-js: + filter: rjsmin + output: ckanext-admin_panel/%(version)s-file-manager.js + contents: + - js/fm-htmx.js + extra: + preload: + - base/main + - base/ckan diff --git a/ckanext/file_manager/col_renderers.py b/ckanext/file_manager/col_renderers.py new file mode 100644 index 0000000..be74228 --- /dev/null +++ b/ckanext/file_manager/col_renderers.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from datetime import datetime + +import ckan.plugins.toolkit as tk + +from ckanext.toolbelt.decorators import Collector + +import ckanext.ap_main.types as ap_types + +renderer, get_renderers = Collector("fm").split() + + +@renderer +def last_access( + rows: ap_types.ItemList, + row: ap_types.Item, + value: ap_types.ItemValue, + **kwargs, +) -> int: + if not value: + return tk._("Never") + + datetime_obj = datetime.fromisoformat(value) + current_date = datetime.now() + + days_passed = (current_date - datetime_obj).days + + return days_passed diff --git a/ckanext/file_manager/collection.py b/ckanext/file_manager/collection.py index 8d517d9..e8ab6c5 100644 --- a/ckanext/file_manager/collection.py +++ b/ckanext/file_manager/collection.py @@ -1,14 +1,14 @@ from __future__ import annotations from typing import Any +from ckanext.collection.utils.data.model import ModelData from dominate import tags import ckan.plugins.toolkit as tk -import sqlalchemy as sa -from ckanext.collection.types import InputFilter, LinkFilter -from ckanext.collection.utils import Filters, StatementSaData +from ckanext.collection.types import InputFilter, LinkFilter, ButtonFilter +from ckanext.collection.utils import Filters from ckanext.ap_main.collection.base import ( ApCollection, @@ -21,9 +21,17 @@ from ckanext.files.model import File +def file_row_dictizer(serializer: ApHtmxTableSerializer, row: File): + data = row.dictize({}) + data["bulk-action"] = data["id"] + + return data + + class FileManagerCollection(ApCollection[Any]): SerializerFactory = ApHtmxTableSerializer.with_attributes( - record_template="file_manager/record.html" + record_template="file_manager/record.html", + row_dictizer=file_row_dictizer, ) ColumnsFactory = ApColumns.with_attributes( @@ -33,10 +41,11 @@ class FileManagerCollection(ApCollection[Any]): "path", "kind", "uploaded_at", + "last_access", "extras", "row_actions", ], - sortable={"name", "kind", "uploaded_at"}, + sortable={"name", "kind", "uploaded_at", "last_access"}, searchable={"name"}, labels={ "bulk-action": tk.literal( @@ -45,13 +54,14 @@ class FileManagerCollection(ApCollection[Any]): name="bulk_check", id="bulk_check", data_module="ap-bulk-check", - data_module_selector='input[name="id"]', + data_module_selector='input[name="entity_id"]', ) ), "name": "Name", "path": "Path", "kind": "Type", "uploaded_at": "Uploaded At", + "last_access": "Last Access", "extras": "Extras", "row_actions": "Actions", }, @@ -59,22 +69,15 @@ class FileManagerCollection(ApCollection[Any]): serializers={ "uploaded_at": [("date", {})], "extras": [("json_display", {})], + "last_access": [("day_passed", {})], }, ) - DataFactory = StatementSaData.with_attributes( + DataFactory = ModelData.with_attributes( model=File, - use_naive_filters=True, + is_scalar=True, use_naive_search=True, - statement=sa.select( - File.id.label("bulk-action"), - File.id.label("id"), - File.name.label("name"), - File.path.label("path"), - File.kind.label("kind"), - File.uploaded_at.label("uploaded_at"), - File.extras.label("extras"), - ), + use_naive_filters=True, ) FiltersFactory = Filters.with_attributes( @@ -98,7 +101,6 @@ class FileManagerCollection(ApCollection[Any]): "icon": "fa fa-pencil", "params": { "data-module-path": "$id", - "entity_type": "$type", "view": "edit", }, }, @@ -126,13 +128,15 @@ class FileManagerCollection(ApCollection[Any]): "placeholder": "Search", }, ), - LinkFilter( - name="clear", - type="link", + ButtonFilter( + name="type", + type="button", options={ "label": "Clear", - "endpoint": "file_manager.list", - "kwargs": {}, + "type": "button", + "attrs": { + "onclick": "$(this).closest('form').find('input,select').val('').prevObject[0].requestSubmit()" + }, }, ), ], diff --git a/ckanext/file_manager/plugin.py b/ckanext/file_manager/plugin.py index 6abf7d9..a779719 100644 --- a/ckanext/file_manager/plugin.py +++ b/ckanext/file_manager/plugin.py @@ -5,10 +5,12 @@ import ckanext.ap_main.types as ap_types from ckanext.ap_main.interfaces import IAdminPanel +from ckanext.ap_main.types import ColRenderer from ckanext.collection.interfaces import ICollection, CollectionFactory from ckanext.file_manager.collection import FileManagerCollection +from ckanext.file_manager.col_renderers import get_renderers @tk.blanket.blueprints @@ -43,6 +45,9 @@ def register_config_sections( ) return config_list + def get_col_renderers(self) -> dict[str, ColRenderer]: + return get_renderers() + # ICollection def get_collection_factories(self) -> dict[str, CollectionFactory]: diff --git a/ckanext/file_manager/templates/file_manager/list.html b/ckanext/file_manager/templates/file_manager/list.html index 72665c8..491ea48 100644 --- a/ckanext/file_manager/templates/file_manager/list.html +++ b/ckanext/file_manager/templates/file_manager/list.html @@ -1,17 +1,13 @@ {% extends 'admin_panel/base.html' %} -{% import 'macros/autoform.html' as autoform %} -{% import 'macros/form.html' as form %} -{% import 'admin_panel/macros/form.html' as ap_form %} - -{% block ap_main_class %} ap-log-list {% endblock %} +{% block ap_main_class %} file-manager-list {% endblock %} {% block breadcrumb_content %}
  • {% link_for _("File manager"), request.endpoint %}
  • {% endblock breadcrumb_content %} {% block ap_content %} -
    +