diff --git a/.github/workflows/test_js.yaml b/.github/workflows/test_js.yaml new file mode 100644 index 0000000..3fa3ea4 --- /dev/null +++ b/.github/workflows/test_js.yaml @@ -0,0 +1,25 @@ +name: JS tests + +on: [push] + +jobs: + test: + name: Test + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Corepack + run: | + npm install --global corepack@latest + corepack enable + + - name: Install + run: | + corepack enable + make nodejs + + - name: Run tests + run: make wtr diff --git a/.github/workflows/test_py.yaml b/.github/workflows/test_py.yaml new file mode 100644 index 0000000..c9b2d91 --- /dev/null +++ b/.github/workflows/test_py.yaml @@ -0,0 +1,30 @@ +name: Test yafowil.widget.cron + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + + steps: + - uses: actions/checkout@v5 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Show Python version + run: python -c "import sys; print(sys.version)" + + - name: Run tests an collect code coverage + run: make coverage diff --git a/.gitignore b/.gitignore index 6d485f1..54d2b66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,19 @@ -*~ -*#* +*.egg-info *.pyc *.pyo -*.egg-info -/develop-eggs/ -/parts/ -/bin/ -/eggs/ -/downloads/ -/var/ -/dist/ +/.coverage +/.mxmake/ +/build +/constraints-mxdev.txt /coverage/ -/devsrc/ -/include/ -/lib/ -/local/ -/share/ -/.installed.cfg -/.pydevproject -/.project -/.DS_Store -/.mr.developer.cfg +/dist/ +/htmlcov/ +/node_modules/ +/package-lock.json +/pnpm-lock.yaml +/py2/ +/py3/ +/pypy3/ +/requirements-mxdev.txt +/sources/ +/venv/ diff --git a/CHANGES.rst b/CHANGES.rst index c3bb19e..015c94b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,10 +1,45 @@ Changes ======= -1.3 (unreleased) ----------------- +2.0.0 (unreleased) +------------------ + +- Update jQuery to version ``4.0.0-beta.2``. + [lenadax] + +- Add new widget options and defaults for easier styling and customization: + ``edit_container_class`` + ``options_container_class`` + ``options_header_class`` + ``edit_btn_class`` + ``editarea_class`` + [lenadax] + +- Use rollup for bundling scss. Use ``make rollup`` to compile js and scss. + [lenadax] + +- Use ``webtestrunner`` instead of ``karma`` for js tests. Use ``make wtr`` to run tests. + [lenadax] + +- Use ``pnpm`` as package manager. + [lenadax] + +- Create Bootstrap5 widget version. + [lenadax] -- No changes yet. + +2.0a1 (2023-05-15) +------------------ + +- Add ``webresource`` support. + [rnix] + +- Extend JS by ``cron_on_array_add`` and ``register_array_subscribers`` + functions to enable usage in ``yafowil.widget.array``. + [lenadax] + +- Rewrite JavaScript using ES6. + [rnix] 1.2 (2020-05-30) diff --git a/LICENSE.rst b/LICENSE.rst index 9032682..08a1ec0 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -2,7 +2,7 @@ License ======= Copyright (c) 2010-2021, BlueDynamics Alliance, Austria, Germany, Switzerland -Copyright (c) 2021, Yafowil Contributors +Copyright (c) 2021-2024, Yafowil Contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index da5d1de..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,3 +0,0 @@ -include *.rst -recursive-include src * -recursive-exclude src *.pyc *.pyo diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..530ad81 --- /dev/null +++ b/Makefile @@ -0,0 +1,821 @@ +############################################################################## +# THIS FILE IS GENERATED BY MXMAKE +# +# DOMAINS: +#: applications.zest-releaser +#: core.base +#: core.mxenv +#: core.mxfiles +#: core.packages +#: core.sources +#: i18n.gettext +#: i18n.lingua +#: js.nodejs +#: js.rollup +#: js.wtr +#: qa.coverage +#: qa.test +# +# SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST) +############################################################################## + +## core.base + +# `deploy` target dependencies. +# No default value. +DEPLOY_TARGETS?= + +# target to be executed when calling `make run` +# No default value. +RUN_TARGET?= + +# Additional files and folders to remove when running clean target +# No default value. +CLEAN_FS?= + +# Optional makefile to include before default targets. This can +# be used to provide custom targets or hook up to existing targets. +# Default: include.mk +INCLUDE_MAKEFILE?=include.mk + +# Optional additional directories to be added to PATH in format +# `/path/to/dir/:/path/to/other/dir`. Gets inserted first, thus gets searched +# first. +# No default value. +EXTRA_PATH?= + +# Path to Python project relative to Makefile (repository root). +# Leave empty if Python project is in the same directory as Makefile. +# For monorepo setups, set to subdirectory name (e.g., `backend`). +# Future-proofed for multi-language monorepos (e.g., PROJECT_PATH_NODEJS). +# No default value. +PROJECT_PATH_PYTHON?= + +## js.nodejs + +# The package manager to use. Defaults to `npm`. Possible values +# are `npm` and `pnpm` +# Default: npm +NODEJS_PACKAGE_MANAGER?=pnpm + +# Value for `--prefix` option when installing packages. +# Default: . +NODEJS_PREFIX?=. + +# Packages to install with `--no-save` option. +# No default value. +NODEJS_PACKAGES?= + +# Packages to install with `--save-dev` option. +# No default value. +NODEJS_DEV_PACKAGES?= + +# Packages to install with `--save-prod` option. +# No default value. +NODEJS_PROD_PACKAGES?= + +# Packages to install with `--save-optional` option. +# No default value. +NODEJS_OPT_PACKAGES?= + +# Additional install options. Possible values are `--save-exact` +# and `--save-bundle`. +# No default value. +NODEJS_INSTALL_OPTS?= + +## js.wtr + +# Web test runner config file. +# Default: wtr.config.mjs +WTR_CONFIG?=js/wtr.config.mjs + +# Web test runner additional command line options. +# Default: --coverage +WTR_OPTIONS?=--coverage + +## js.rollup + +# Rollup config file. +# Default: rollup.conf.js +ROLLUP_CONFIG?=js/rollup.conf.js + +## core.mxenv + +# Primary Python interpreter to use. It is used to create the +# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`. +# If global `uv` is used, this value is passed as `--python VALUE` to the venv creation. +# uv then downloads the Python interpreter if it is not available. +# for more on this feature read the [uv python documentation](https://docs.astral.sh/uv/concepts/python-versions/) +# Default: python3 +PRIMARY_PYTHON?=python3 + +# Minimum required Python version. +# Default: 3.10 +PYTHON_MIN_VERSION?=3.10 + +# Install packages using the given package installer method. +# Supported are `pip` and `uv`. When `uv` is selected, a global installation +# is auto-detected and used if available. Otherwise, uv is installed in the +# virtual environment or using `PRIMARY_PYTHON`, depending on the +# `VENV_ENABLED` setting. +# Default: pip +PYTHON_PACKAGE_INSTALLER?=uv + +# Python version for UV to install/use when creating virtual +# environments with global UV. Passed to `uv venv -p VALUE`. Supports version +# specs like `3.11`, `3.14`, `cpython@3.14`. Defaults to PRIMARY_PYTHON value +# for backward compatibility. +# Default: $(PRIMARY_PYTHON) +UV_PYTHON?=$(PRIMARY_PYTHON) + +# Flag whether to use virtual environment. If `false`, the +# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. +# Default: true +VENV_ENABLED?=true + +# Flag whether to create a virtual environment. If set to `false` +# and `VENV_ENABLED` is `true`, `VENV_FOLDER` is expected to point to an +# existing virtual environment. +# Default: true +VENV_CREATE?=true + +# The folder of the virtual environment. +# If `VENV_ENABLED` is `true` and `VENV_CREATE` is true it is used as the +# target folder for the virtual environment. If `VENV_ENABLED` is `true` and +# `VENV_CREATE` is false it is expected to point to an existing virtual +# environment. If `VENV_ENABLED` is `false` it is ignored. +# Default: .venv +VENV_FOLDER?=venv + +# mxdev to install in virtual environment. +# Default: mxdev +MXDEV?=mxdev + +# mxmake to install in virtual environment. +# Default: mxmake +MXMAKE?=mxmake + +## core.mxfiles + +# The config file to use. +# Default: mx.ini +PROJECT_CONFIG?=mx.ini + +## core.packages + +# Allow prerelease and development versions. +# By default, the package installer only finds stable versions. +# Default: false +PACKAGES_ALLOW_PRERELEASES?=false + +## qa.test + +# The command which gets executed. Defaults to the location the +# :ref:`run-tests` template gets rendered to if configured. +# Default: .mxmake/files/run-tests.sh +TEST_COMMAND?=.mxmake/files/run-tests.sh + +# Additional Python requirements for running tests to be +# installed (via pip). +# Default: pytest +TEST_REQUIREMENTS?=pytest + +# Additional make targets the test target depends on. +# No default value. +TEST_DEPENDENCY_TARGETS?= + +## qa.coverage + +# The command which gets executed. Defaults to the location the +# :ref:`run-coverage` template gets rendered to if configured. +# Default: .mxmake/files/run-coverage.sh +COVERAGE_COMMAND?=.mxmake/files/run-coverage.sh + +## applications.zest-releaser + +# Options to pass to zest.releaser prerelease command. +# No default value. +ZEST_RELEASER_PRERELEASE_OPTIONS?= + +# Options to pass to zest.releaser release command. +# No default value. +ZEST_RELEASER_RELEASE_OPTIONS?= + +# Options to pass to zest.releaser postrelease command. +# No default value. +ZEST_RELEASER_POSTRELEASE_OPTIONS?= + +# Options to pass to zest.releaser fullrelease command. +# No default value. +ZEST_RELEASER_FULLRELEASE_OPTIONS?= + +## i18n.gettext + +# Path of directory containing the message catalogs. +# Default: locale +GETTEXT_LOCALES_PATH?=src/yafowil/widget/cron/locales + +# Translation domain to use. +# No default value. +GETTEXT_DOMAIN?=yafowil.widget.cron + +# Space separated list of language identifiers. +# No default value. +GETTEXT_LANGUAGES?=de en + +## i18n.lingua + +# Path of directory to extract translatable texts from. +# Default: src +LINGUA_SEARCH_PATH?=src + +# Python packages containing lingua extensions. +# No default value. +LINGUA_PLUGINS?= + +# Command line options passed to `pot-create` +# No default value. +LINGUA_OPTIONS?= + +############################################################################## +# END SETTINGS - DO NOT EDIT BELOW THIS LINE +############################################################################## + +INSTALL_TARGETS?= +DIRTY_TARGETS?= +CLEAN_TARGETS?= +PURGE_TARGETS?= +CHECK_TARGETS?= +TYPECHECK_TARGETS?= +FORMAT_TARGETS?= + +export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH) + +# Helper variable: adds trailing slash to PROJECT_PATH_PYTHON only if non-empty +PYTHON_PROJECT_PREFIX=$(if $(PROJECT_PATH_PYTHON),$(PROJECT_PATH_PYTHON)/,) + +# Defensive settings for make: https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# mxmake folder +MXMAKE_FOLDER?=.mxmake + +# Sentinel files +SENTINEL_FOLDER?=$(MXMAKE_FOLDER)/sentinels +SENTINEL?=$(SENTINEL_FOLDER)/about.txt +$(SENTINEL): $(firstword $(MAKEFILE_LIST)) + @mkdir -p $(SENTINEL_FOLDER) + @echo "Sentinels for the Makefile process." > $(SENTINEL) + +############################################################################## +# nodejs +############################################################################## + +export PATH:=$(shell pwd)/$(NODEJS_PREFIX)/node_modules/.bin:$(PATH) + + +NODEJS_TARGET:=$(SENTINEL_FOLDER)/nodejs.sentinel +$(NODEJS_TARGET): $(SENTINEL) + @echo "Install nodejs packages" + @test -z "$(NODEJS_DEV_PACKAGES)" \ + && echo "No dev packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-dev \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_DEV_PACKAGES) + @test -z "$(NODEJS_PROD_PACKAGES)" \ + && echo "No prod packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-prod \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_PROD_PACKAGES) + @test -z "$(NODEJS_OPT_PACKAGES)" \ + && echo "No opt packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-optional \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_OPT_PACKAGES) + @test -z "$(NODEJS_PACKAGES)" \ + && echo "No packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --no-save \ + $(NODEJS_PACKAGES) + @touch $(NODEJS_TARGET) + +.PHONY: nodejs +nodejs: $(NODEJS_TARGET) + +.PHONY: nodejs-dirty +nodejs-dirty: + @rm -f $(NODEJS_TARGET) + +.PHONY: nodejs-clean +nodejs-clean: nodejs-dirty + @rm -rf $(NODEJS_PREFIX)/node_modules + +INSTALL_TARGETS+=nodejs +DIRTY_TARGETS+=nodejs-dirty +CLEAN_TARGETS+=nodejs-clean + +############################################################################## +# web test runner +############################################################################## + +NODEJS_DEV_PACKAGES+=\ + @web/test-runner \ + @web/dev-server-import-maps + +.PHONY: wtr +wtr: $(NODEJS_TARGET) + @web-test-runner $(WTR_OPTIONS) --config $(WTR_CONFIG) + +############################################################################## +# rollup +############################################################################## + +NODEJS_DEV_PACKAGES+=\ + rollup \ + rollup-plugin-cleanup \ + @rollup/plugin-terser + +.PHONY: rollup +rollup: $(NODEJS_TARGET) + @rollup --config $(ROLLUP_CONFIG) + +############################################################################## +# mxenv +############################################################################## + +OS?= + +# Determine the executable path +ifeq ("$(VENV_ENABLED)", "true") +export VIRTUAL_ENV=$(abspath $(VENV_FOLDER)) +ifeq ("$(OS)", "Windows_NT") +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/Scripts +else +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/bin +endif +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) +MXENV_PYTHON=python +else +MXENV_PYTHON=$(PRIMARY_PYTHON) +endif + +# Determine the package installer with non-interactive flags +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PYTHON_PACKAGE_COMMAND=uv pip --no-progress +else +PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip +endif + +# Auto-detect global uv availability (simple existence check) +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +UV_AVAILABLE:=$(shell command -v uv >/dev/null 2>&1 && echo "true" || echo "false") +else +UV_AVAILABLE:=false +endif + +# Determine installation strategy +# depending on the PYTHON_PACKAGE_INSTALLER and UV_AVAILABLE +# - both vars can be false or +# - one of them can be true, +# - but never boths. +USE_GLOBAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "true" ]] && echo "true" || echo "false") +USE_LOCAL_UV:=$(shell [[ "$(PYTHON_PACKAGE_INSTALLER)" == "uv" && "$(UV_AVAILABLE)" == "false" ]] && echo "true" || echo "false") + +# Check if global UV is outdated (non-blocking warning) +ifeq ("$(USE_GLOBAL_UV)","true") +UV_OUTDATED:=$(shell uv self update --dry-run 2>&1 | grep -q "Would update" && echo "true" || echo "false") +else +UV_OUTDATED:=false +endif + +MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel +$(MXENV_TARGET): $(SENTINEL) + # Validation: Check Python version if not using global uv +ifneq ("$(USE_GLOBAL_UV)","true") + @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ + && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : +else + @echo "Using global uv for Python $(UV_PYTHON)" +endif + # Validation: Check VENV_FOLDER is set if venv enabled + @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ + && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : + # Validation: Check uv not used with system Python + @[[ "$(VENV_ENABLED)" == "false" && "$(PYTHON_PACKAGE_INSTALLER)" == "uv" ]] \ + && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : + # Warning: Notify if global UV is outdated +ifeq ("$(UV_OUTDATED)","true") + @echo "WARNING: A newer version of uv is available. Run 'uv self update' to upgrade." +endif + + # Create virtual environment +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") +ifeq ("$(USE_GLOBAL_UV)","true") + @echo "Setup Python Virtual Environment using global uv at '$(VENV_FOLDER)'" + @uv venv --allow-existing --no-progress -p $(UV_PYTHON) --seed $(VENV_FOLDER) +else + @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" + @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) + @$(MXENV_PYTHON) -m ensurepip -U +endif +endif +else + @echo "Using system Python interpreter" +endif + + # Install uv locally if needed +ifeq ("$(USE_LOCAL_UV)","true") + @echo "Install uv in virtual environment" + @$(MXENV_PYTHON) -m pip install uv +endif + + # Install/upgrade core packages + @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel + @echo "Install/Update MXStack Python packages" + @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) + @touch $(MXENV_TARGET) + +.PHONY: mxenv +mxenv: $(MXENV_TARGET) + +.PHONY: mxenv-dirty +mxenv-dirty: + @rm -f $(MXENV_TARGET) + +.PHONY: mxenv-clean +mxenv-clean: mxenv-dirty +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") + @rm -rf $(VENV_FOLDER) +endif +else + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXDEV) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXMAKE) +endif + +INSTALL_TARGETS+=mxenv +DIRTY_TARGETS+=mxenv-dirty +CLEAN_TARGETS+=mxenv-clean + +############################################################################## +# sources +############################################################################## + +SOURCES_TARGET:=$(SENTINEL_FOLDER)/sources.sentinel +$(SOURCES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) + @echo "Checkout project sources" + @mxdev -f -c $(PROJECT_CONFIG) + @touch $(SOURCES_TARGET) + +.PHONY: sources +sources: $(SOURCES_TARGET) + +.PHONY: sources-dirty +sources-dirty: + @rm -f $(SOURCES_TARGET) + +.PHONY: sources-purge +sources-purge: sources-dirty + @rm -rf sources + +INSTALL_TARGETS+=sources +DIRTY_TARGETS+=sources-dirty +PURGE_TARGETS+=sources-purge + +############################################################################## +# mxfiles +############################################################################## + +# case `core.sources` domain not included +SOURCES_TARGET?= + +# File generation target +MXMAKE_FILES?=$(MXMAKE_FOLDER)/files + +# set environment variables for mxmake +define set_mxfiles_env + @export MXMAKE_FILES=$(1) +endef + +# unset environment variables for mxmake +define unset_mxfiles_env + @unset MXMAKE_FILES +endef + +$(PROJECT_CONFIG): +ifneq ("$(wildcard $(PROJECT_CONFIG))","") + @touch $(PROJECT_CONFIG) +else + @echo "[settings]" > $(PROJECT_CONFIG) +endif + +LOCAL_PACKAGE_FILES:=$(wildcard $(PYTHON_PROJECT_PREFIX)pyproject.toml $(PYTHON_PROJECT_PREFIX)setup.cfg $(PYTHON_PROJECT_PREFIX)setup.py $(PYTHON_PROJECT_PREFIX)requirements.txt $(PYTHON_PROJECT_PREFIX)constraints.txt) + +FILES_TARGET:=requirements-mxdev.txt +$(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES) + @echo "Create project files" + @mkdir -p $(MXMAKE_FILES) + $(call set_mxfiles_env,$(MXMAKE_FILES)) + @mxdev -n -c $(PROJECT_CONFIG) + $(call unset_mxfiles_env) + @test -e $(MXMAKE_FILES)/pip.conf && cp $(MXMAKE_FILES)/pip.conf $(VENV_FOLDER)/pip.conf || : + @touch $(FILES_TARGET) + +.PHONY: mxfiles +mxfiles: $(FILES_TARGET) + +.PHONY: mxfiles-dirty +mxfiles-dirty: + @touch $(PROJECT_CONFIG) + +.PHONY: mxfiles-clean +mxfiles-clean: mxfiles-dirty + @rm -rf constraints-mxdev.txt requirements-mxdev.txt $(MXMAKE_FILES) + +INSTALL_TARGETS+=mxfiles +DIRTY_TARGETS+=mxfiles-dirty +CLEAN_TARGETS+=mxfiles-clean + +############################################################################## +# packages +############################################################################## + +# additional sources targets which requires package re-install on change +-include $(MXMAKE_FILES)/additional_sources_targets.mk +ADDITIONAL_SOURCES_TARGETS?= + +INSTALLED_PACKAGES=$(MXMAKE_FILES)/installed.txt + +ifeq ("$(PACKAGES_ALLOW_PRERELEASES)","true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PACKAGES_PRERELEASES=--prerelease=allow +else +PACKAGES_PRERELEASES=--pre +endif +else +PACKAGES_PRERELEASES= +endif + +PACKAGES_TARGET:=$(INSTALLED_PACKAGES) +$(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS) + @echo "Install python packages" + @$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET) + @$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES) + @touch $(PACKAGES_TARGET) + +.PHONY: packages +packages: $(PACKAGES_TARGET) + +.PHONY: packages-dirty +packages-dirty: + @rm -f $(PACKAGES_TARGET) + +.PHONY: packages-clean +packages-clean: + @test -e $(FILES_TARGET) \ + && test -e $(MXENV_PYTHON) \ + && $(MXENV_PYTHON) -m pip uninstall -y -r $(FILES_TARGET) \ + || : + @rm -f $(PACKAGES_TARGET) + +INSTALL_TARGETS+=packages +DIRTY_TARGETS+=packages-dirty +CLEAN_TARGETS+=packages-clean + +############################################################################## +# test +############################################################################## + +TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel +$(TEST_TARGET): $(MXENV_TARGET) + @echo "Install $(TEST_REQUIREMENTS)" + @$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS) + @touch $(TEST_TARGET) + +.PHONY: test +test: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(TEST_TARGET) $(TEST_DEPENDENCY_TARGETS) + @test -z "$(TEST_COMMAND)" && echo "No test command defined" && exit 1 || : + @echo "Run tests using $(TEST_COMMAND)" + @/usr/bin/env bash -c "$(TEST_COMMAND)" + +.PHONY: test-dirty +test-dirty: + @rm -f $(TEST_TARGET) + +.PHONY: test-clean +test-clean: test-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || : + @rm -rf .pytest_cache + +INSTALL_TARGETS+=$(TEST_TARGET) +CLEAN_TARGETS+=test-clean +DIRTY_TARGETS+=test-dirty + +############################################################################## +# coverage +############################################################################## + +COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel +$(COVERAGE_TARGET): $(TEST_TARGET) + @echo "Install Coverage" + @$(PYTHON_PACKAGE_COMMAND) install -U coverage + @touch $(COVERAGE_TARGET) + +.PHONY: coverage +coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) + @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" && exit 1 || : + @echo "Run coverage using $(COVERAGE_COMMAND)" + @/usr/bin/env bash -c "$(COVERAGE_COMMAND)" + +.PHONY: coverage-dirty +coverage-dirty: + @rm -f $(COVERAGE_TARGET) + +.PHONY: coverage-clean +coverage-clean: coverage-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : + @rm -rf .coverage htmlcov + +INSTALL_TARGETS+=$(COVERAGE_TARGET) +DIRTY_TARGETS+=coverage-dirty +CLEAN_TARGETS+=coverage-clean + +############################################################################## +# zest-releaser +############################################################################## + +ZEST_RELEASER_TARGET:=$(SENTINEL_FOLDER)/zest-releaser.sentinel +$(ZEST_RELEASER_TARGET): $(MXENV_TARGET) + @echo "Install zest.releaser" + @$(PYTHON_PACKAGE_COMMAND) install zest.releaser + @touch $(ZEST_RELEASER_TARGET) + +.PHONY: zest-releaser-prerelease +zest-releaser-prerelease: $(ZEST_RELEASER_TARGET) + @echo "Run prerelease" + @prerelease $(ZEST_RELEASER_PRERELEASE_OPTIONS) + +.PHONY: zest-releaser-release +zest-releaser-release: $(ZEST_RELEASER_TARGET) + @echo "Run release" + @release $(ZEST_RELEASER_RELEASE_OPTIONS) + +.PHONY: zest-releaser-postrelease +zest-releaser-postrelease: $(ZEST_RELEASER_TARGET) + @echo "Run postrelease" + @postrelease $(ZEST_RELEASER_POSTRELEASE_OPTIONS) + +.PHONY: zest-releaser-fullrelease +zest-releaser-fullrelease: $(ZEST_RELEASER_TARGET) + @echo "Run fullrelease" + @fullrelease $(ZEST_RELEASER_FULLRELEASE_OPTIONS) + +.PHONY: zest-releaser-dirty +zest-releaser-dirty: + @rm -f $(ZEST_RELEASER_TARGET) + +.PHONY: zest-releaser-clean +zest-releaser-clean: zest-releaser-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y zest.releaser || : + +INSTALL_TARGETS+=$(ZEST_RELEASER_TARGET) +DIRTY_TARGETS+=zest-releaser-dirty +CLEAN_TARGETS+=zest-releaser-clean + +############################################################################## +# gettext +############################################################################## + +# case `system.dependencies` domain is included +SYSTEM_DEPENDENCIES+=gettext + +.PHONY: gettext-create +gettext-create: + @if [ ! -e "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" ]; then \ + echo "Create pot file"; \ + mkdir -p "$(GETTEXT_LOCALES_PATH)"; \ + touch "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot"; \ + fi + @for lang in $(GETTEXT_LANGUAGES); do \ + if [ ! -e "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" ]; then \ + mkdir -p "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES"; \ + msginit \ + -i "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" \ + -o "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + -l $$lang; \ + fi \ + done + +.PHONY: gettext-update +gettext-update: + @echo "Update translations" + @for lang in $(GETTEXT_LANGUAGES); do \ + msgmerge -o \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po" \ + "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot"; \ + done + +.PHONY: gettext-compile +gettext-compile: + @echo "Compile message catalogs" + @for lang in $(GETTEXT_LANGUAGES); do \ + msgfmt --statistics -o \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).mo" \ + "$(GETTEXT_LOCALES_PATH)/$$lang/LC_MESSAGES/$(GETTEXT_DOMAIN).po"; \ + done + +############################################################################## +# lingua +############################################################################## + +LINGUA_TARGET:=$(SENTINEL_FOLDER)/lingua.sentinel +$(LINGUA_TARGET): $(MXENV_TARGET) + @echo "Install Lingua" + @$(PYTHON_PACKAGE_COMMAND) install chameleon lingua $(LINGUA_PLUGINS) + @touch $(LINGUA_TARGET) + +PHONY: lingua-extract +lingua-extract: $(LINGUA_TARGET) + @echo "Extract messages" + @pot-create \ + "$(LINGUA_SEARCH_PATH)" $(LINGUA_OPTIONS) \ + -o "$(GETTEXT_LOCALES_PATH)/$(GETTEXT_DOMAIN).pot" + +PHONY: lingua +lingua: gettext-create lingua-extract gettext-update gettext-compile + +.PHONY: lingua-dirty +lingua-dirty: + @rm -f $(LINGUA_TARGET) + +.PHONY: lingua-clean +lingua-clean: lingua-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y \ + chameleon lingua $(LINGUA_PLUGINS) || : + +INSTALL_TARGETS+=$(LINGUA_TARGET) +DIRTY_TARGETS+=lingua-dirty +CLEAN_TARGETS+=lingua-clean + +############################################################################## +# Custom includes +############################################################################## + +-include $(INCLUDE_MAKEFILE) + +############################################################################## +# Default targets +############################################################################## + +INSTALL_TARGET:=$(SENTINEL_FOLDER)/install.sentinel +$(INSTALL_TARGET): $(INSTALL_TARGETS) + @touch $(INSTALL_TARGET) + +.PHONY: install +install: $(INSTALL_TARGET) + @touch $(INSTALL_TARGET) + +.PHONY: run +run: $(RUN_TARGET) + +.PHONY: deploy +deploy: $(DEPLOY_TARGETS) + +.PHONY: dirty +dirty: $(DIRTY_TARGETS) + @rm -f $(INSTALL_TARGET) + +.PHONY: clean +clean: dirty $(CLEAN_TARGETS) + @rm -rf $(CLEAN_TARGETS) $(MXMAKE_FOLDER) $(CLEAN_FS) + +.PHONY: purge +purge: clean $(PURGE_TARGETS) + +.PHONY: runtime-clean +runtime-clean: + @echo "Remove runtime artifacts, like byte-code and caches." + @find . -name '*.py[c|o]' -delete + @find . -name '*~' -exec rm -f {} + + @find . -name '__pycache__' -exec rm -fr {} + + +.PHONY: check +check: $(CHECK_TARGETS) + +.PHONY: typecheck +typecheck: $(TYPECHECK_TARGETS) + +.PHONY: format +format: $(FORMAT_TARGETS) diff --git a/README.rst b/README.rst index de69524..facfa5c 100644 --- a/README.rst +++ b/README.rst @@ -20,3 +20,4 @@ Contributors - Georg Bernhard +- Lena Daxenbichler diff --git a/TODO.rst b/TODO.rst deleted file mode 100644 index 6c1e0e5..0000000 --- a/TODO.rst +++ /dev/null @@ -1,39 +0,0 @@ - -- userselect - disablen, wenn man mit mouse down aus -- tab switchen - editarea setzt sich zurück - -https://github.com/josiahcarlson/parse-crontab -https://pypi.python.org/pypi/crontab - -- [x] Buttonfarbe, -- [x] 7er block bei dom -- [x] month: 6er block (oder 3) -- [x] year: alles vor aktuellem jahr weg. - -- [x] inputs schmäler, label davor, edit hinten -- [x] inputs inaktiv? - -- [x] buttons weniger minesweeper, mehr bootstrap (eventuell wegen active class?) -- [x] bootstrap basis style, hintergrundfarbe - -obsolete: - -- bootstrap btn (yafowil.plone) via factory defaults. - -nice to have: - -- widget auflösen? -- ranges,...? -... touchup ? -... extractor + render value translator - -- document the 4 test cases from widgets.rst in yafowil main docs: - - extraction without preset value and request form set, - - extraction without preset value but request form set, - - extraction with preset value but no request form set, - - extraction with preset value and request form set. - -- verify: - "This causes the callable chains of each blueprint beeing executed in order. Extractors are executed from right to left while all others are executed left to right." - it seems to be the other way around. - http://docs.yafowil.info/architecture.html diff --git a/bootstrap.sh b/bootstrap.sh deleted file mode 100755 index d764997..0000000 --- a/bootstrap.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -# see https://community.plone.org/t/not-using-bootstrap-py-as-default/620 -rm -r ./lib ./include ./local ./bin -virtualenv --clear . -./bin/pip install --upgrade pip setuptools zc.buildout -./bin/buildout diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index c3c7e2e..0000000 --- a/buildout.cfg +++ /dev/null @@ -1,32 +0,0 @@ -[buildout] -parts = test coverage py -develop = . - -extensions = mr.developer -sources-dir = ${buildout:directory}/devsrc -always-checkout = force -auto-checkout = * - -[remotes] -cs = git://github.com/conestack -cs_push = git@github.com:conestack - -[sources] -yafowil = git ${remotes:cs}/yafowil.git pushurl=${remotes:cs_push}/yafowil.git - -[test] -recipe = zc.recipe.testrunner -eggs = - yafowil[test] - yafowil.widget.cron[test] -defaults = ['--auto-color', '--auto-progress'] - -[coverage] -recipe = zc.recipe.testrunner -eggs = ${test:eggs} -defaults = ['--coverage', '../../coverage', '-v', '--auto-progress'] - -[py] -recipe = zc.recipe.egg -eggs = ${test:eggs} -interpreter = py diff --git a/i18n.sh b/i18n.sh deleted file mode 100755 index d8ae972..0000000 --- a/i18n.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# Author: Robert Niederreiter -# License: no license -# Date: 2014-07-20 -# -# Requirements: -# - python lingua (sudo pip install lingua) -# - gettext -# -# Usage: -# Initial catalog creation (lang is the language identifier): -# ./i18n.sh lang -# Updating translation and compile catalog: -# ./i18n.sh - -# configuration -DOMAIN="yafowil.widget.cron" -SEACH_PATH=src/yafowil/widget/cron -LOCALES_PATH=src/yafowil/widget/cron/locales -# end configuration - -# create locales folder if not exists -if [ ! -d "$LOCALES_PATH" ]; then - echo "Locales directory not exists, create" - mkdir -p $LOCALES_PATH -fi - -# create pot if not exists -if [ ! -f $LOCALES_PATH/$DOMAIN.pot ]; then - echo "Create pot file" - touch $LOCALES_PATH/$DOMAIN.pot -fi - -# no arguments, extract and update -if [ $# -eq 0 ]; then - echo "Extract messages" - pot-create $SEACH_PATH -o $LOCALES_PATH/$DOMAIN.pot - - echo "Update translations" - for po in $LOCALES_PATH/*/LC_MESSAGES/$DOMAIN.po; do - msgmerge -o $po $po $LOCALES_PATH/$DOMAIN.pot - done - - echo "Compile message catalogs" - for po in $LOCALES_PATH/*/LC_MESSAGES/*.po; do - msgfmt -o ${po%.*}.mo $po - done - -# first argument represents language identifier, create catalog -else - cd $LOCALES_PATH - mkdir -p $1/LC_MESSAGES - msginit -i $DOMAIN.pot -o $1/LC_MESSAGES/$DOMAIN.po -l $1 -fi diff --git a/js/rollup.conf.js b/js/rollup.conf.js new file mode 100644 index 0000000..54e52cb --- /dev/null +++ b/js/rollup.conf.js @@ -0,0 +1,113 @@ +import cleanup from 'rollup-plugin-cleanup'; +import postcss from 'rollup-plugin-postcss'; +import terser from '@rollup/plugin-terser'; + +const out_dir = 'src/yafowil/widget/cron/resources'; + +const outro = ` +window.yafowil = window.yafowil || {}; +window.yafowil.cron = exports; +`; + +export default args => { + let conf = []; + + //////////////////////////////////////////////////////////////////////////// + // DEFAULT + //////////////////////////////////////////////////////////////////////////// + + let bundle_default = { + input: 'js/src/default/bundle.js', + plugins: [ + cleanup() + ], + output: [{ + name: 'yafowil_cron', + file: `${out_dir}/default/widget.js`, + format: 'iife', + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }], + external: [ + 'jquery' + ] + }; + if (args.configDebug !== true) { + bundle_default.output.push({ + name: 'yafowil_cron', + file: `${out_dir}/default/widget.min.js`, + format: 'iife', + plugins: [ + terser() + ], + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }); + } + let scss_default = { + input: ['scss/default/widget.scss'], + output: [{ + file: `${out_dir}/default/widget.min.css`, + format: 'es', + plugins: [terser()], // Optional: Minify the output + }], + plugins: [ + postcss({ + extract: true, + minimize: true, + use: [ + ['sass', { outputStyle: 'compressed' }], + ], + }), + ], + }; + conf.push(bundle_default, scss_default); + + //////////////////////////////////////////////////////////////////////////// + // BOOTSTRAP5 + //////////////////////////////////////////////////////////////////////////// + + let bundle_bs5 = { + input: 'js/src/bootstrap5/bundle.js', + plugins: [ + cleanup() + ], + output: [{ + name: 'yafowil_cron', + file: `${out_dir}/bootstrap5/widget.js`, + format: 'iife', + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }], + external: [ + 'jquery' + ] + }; + if (args.configDebug !== true) { + bundle_bs5.output.push({ + name: 'yafowil_cron', + file: `${out_dir}/bootstrap5/widget.min.js`, + format: 'iife', + plugins: [ + terser() + ], + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }); + } + conf.push(bundle_bs5); + + return conf; +}; diff --git a/js/src/bootstrap5/bundle.js b/js/src/bootstrap5/bundle.js new file mode 100644 index 0000000..2685201 --- /dev/null +++ b/js/src/bootstrap5/bundle.js @@ -0,0 +1,18 @@ +import $ from 'jquery'; + +import {CronWidget} from './widget.js'; +import {register_array_subscribers} from './widget.js'; + +export * from '../default/i18n.js'; +export * from './widget.js'; + +$(function() { + if (window.ts !== undefined) { + ts.ajax.register(CronWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(CronWidget.initialize, true); + } else { + CronWidget.initialize(); + } + register_array_subscribers(); +}); diff --git a/js/src/bootstrap5/widget.js b/js/src/bootstrap5/widget.js new file mode 100644 index 0000000..a6367a4 --- /dev/null +++ b/js/src/bootstrap5/widget.js @@ -0,0 +1,654 @@ +import $ from 'jquery'; +import {i18n} from '../default/i18n.js'; + +export class CronWidget { + + /** + * Initializes each widget in the given DOM context. + * + * @param {HTMLElement} context - DOM context for initialization. + */ + static initialize(context) { + $('.crontab.widget', context).each(function () { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new CronWidget(elem, 'edit'); + }); + $('.display-crontab.widget', context).each(function () { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new CronWidget(elem, 'display'); + }); + } + + /** + * @param {jQuery} root - The root element for this widget. + * @param {string} mode - The mode of the widget ('edit' or 'display'). + */ + constructor(root, mode) { + root.data('yafowil-cron', this); + this.root = root; + this.mode = mode; + + let lang = root.data('lang'); + this.lang = lang ? lang : 'en'; + + let start_year = root.data('start_year'); + this.start_year = start_year ? start_year : new Date().getFullYear(); + + let end_year = root.data('end_year'); + this.end_year = end_year ? end_year : new Date().getFullYear() + 9; + + this.pressed = false; + + this.value = { + minute: [], + hour: [], + dom: [], + month: [], + dow: [], + year: [] + }; + + let summary_container_template = + '
' + + '
' + + '
' + this.translate('summary') + '
' + + '

' + + '
' + + '
'; + + if (mode === 'display') { + this.parse($('code', root).text()); + root.append($(summary_container_template)); + this.update_summary(); + return; + } + + let that = this; + + $('input[type="hidden"]', root).each(function () { + that.parse_from_input($(this)); + }); + + root.append($(summary_container_template)); + this.update_summary(); + + this.edit_area = $('.editarea', root); + this.editarea_cls = this.edit_area.attr('class'); + + this.edit_area.on('mousedown touchstart', function (evt) { + that.pressed = true; + }); + + $(document).on('mouseup touchend', function (evt) { + that.pressed = false; + }); + + $('button.edit', root).on('click', function(evt) { + evt.preventDefault(); + $('button.edit', root).removeClass('active'); + that.show_edit_section($(this)); + }); + } + + /** + * Displays the edit section corresponding to the clicked button. + * + * @param {jQuery} trigger - The button that triggered this action. + */ + show_edit_section(trigger) { + trigger.addClass('active'); + let cnt, + edit_area = this.edit_area, + container = this.get_edit_section(trigger), + mode = this.get_mode(container); + if (edit_area.is(':visible') && edit_area.hasClass(mode)) { + container.removeClass('active'); + trigger.removeClass('active'); + trigger.blur(); + edit_area.attr('class', this.editarea_cls).html('').hide(); + return; + } + let header = $('

'), + content = $('
'); + if (mode === 'minute') { + header.text(this.translate('select_minutes')); + for (cnt=0; cnt <= 59; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } else if (mode === 'hour') { + header.text(this.translate('select_hours')); + let hour; + for (cnt=1; cnt <= 24; cnt++) { + // "0" should be rendered as last element. + hour = cnt < 24 ? cnt : 0; + content.append(this.make_button(hour, hour, mode)); + } + } else if (mode === 'dom') { + header.text(this.translate('select_dom')); + for (cnt=1; cnt <= 31; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } else if (mode === 'month') { + header.text(this.translate('select_month')); + let monthmap = this.monthmap(); + for (cnt=1; cnt <= 12; cnt++) { + content.append(this.make_button(cnt, monthmap[cnt], mode)); + } + } else if (mode === 'dow') { + header.text(this.translate('select_dow')); + let dowmap = this.dowmap(), + dow; + for (cnt=1; cnt <= 7; cnt++) { + // "0" should be rendered as last element. + dow = cnt < 7 ? cnt : 0; + content.append(this.make_button(dow, dowmap[dow], mode)); + } + } else if (mode === 'year') { + header.text(this.translate('select_year')); + for (cnt=this.start_year; cnt <= this.end_year; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } + header = header.add(this.make_button_all(mode)); + content = header.add(content); + edit_area.html(content).attr('class', this.editarea_cls + ' ' + mode).show(); + container.addClass('active'); + } + + /** + * Creates a button that selects or clears all options for a given mode. + * + * @param {string} mode - The mode for which to create the button. + * @returns {jQuery} - The button element. + */ + make_button_all(mode) { + let button = $( + '' + ); + if (this.value[mode].length >= this.maxlengths()[mode]) { + button.addClass('active'); + } + let that = this; + button.on('click', function(evt) { + evt.preventDefault(); + let $this = $(this); + if ($this.hasClass('active')) { + // clear + $this.parent().find('.editcontainer button').each(function () { + $(this).removeClass('active'); + }); + $this.removeClass('active'); + that.parse_part('', mode); + } else { + // select all + $this.parent().find('.editcontainer button').each(function () { + $(this).removeClass('active').addClass('active'); + }); + $this.removeClass('active').addClass('active'); + that.parse_part('*', mode); + } + that.serialize_to_input(); + that.update_summary(); + }); + return button; + } + + /** + * Creates a button for a specific value. + * + * @param {string} value - The value for the button. + * @param {string} name - The text to display on the button. + * @param {string} mode - The mode for which the button is created. + * @returns {jQuery} - The button element. + */ + make_button(value, name, mode) { + let button = $( + '' + ); + if (this.has(value, mode)) { + button.addClass('active'); + } + let that = this; + + let handler = function(evt) { + evt.preventDefault(); + let elem = $(this); + let container = that.edit_area; + if (elem.hasClass('active')) { + that.remove(elem.attr('name'), that.get_mode(container)); + that.serialize_to_input(); + elem.removeClass('active'); + } else { + that.add(elem.attr('name'), that.get_mode(container)); + that.serialize_to_input(); + elem.addClass('active'); + } + that.update_summary(); + }; + + button.on('click', function (evt) { + evt.preventDefault(); + }).on('mousedown touchstart', + handler + ).on('mouseenter touchenter', function (evt) { + if (evt.shiftKey === false && that.pressed === false) { + return; + } + handler.bind(this)(evt); + }).on('mouseup touchend', function (evt) { + that.pressed = false; + }); + return button; + } + + /** + * Returns the maximum number of selectable values for each mode. + * + * @returns {Object} - An object containing maximum values for each mode. + */ + maxlengths() { + return { + minute: 60, + hour: 24, + dom: 31, + month: 12, + dow: 7, + year: this.end_year - this.start_year + 1 + }; + } + + /** + * Translates a message key into the corresponding language string. + * + * @param {string} msg - The message key to translate. + * @returns {string} - The translated string. + */ + translate(msg) { + return i18n[this.lang][msg]; + } + + + /** + * Gets the month mapping for the current language. + * + * @returns {Object} - An object mapping month numbers to their names + * in the current language. + */ + monthmap() { + return i18n[this.lang].monthmap; + } + + /** + * Gets the day of the week mapping for the current language. + * + * @returns {Object} - An object mapping day of week numbers to their names + * in the current language. + */ + dowmap() { + return i18n[this.lang].dowmap; + } + + /** + * Gets the closest edit section for the specified element. + * + * @param {jQuery} elem - The jQuery element from which to find + * the closest section. + * @returns {jQuery} - The closest section element. + */ + get_edit_section(elem) { + return elem.closest('.cron-value'); + } + + /** + * Acquires the mode of the specified edit section element. + * + * @param {jQuery} elem - The jQuery element to check the mode for. + * @returns {string} - The mode of the element. + */ + get_mode(elem) { + let klass = elem.attr('class'); + if (klass.indexOf('minute') !== -1) { + return 'minute'; + } else if (klass.indexOf('hour') !== -1) { + return 'hour'; + } else if (klass.indexOf('dom') !== -1) { + return 'dom'; + } else if (klass.indexOf('month') !== -1) { + return 'month'; + } else if (klass.indexOf('dow') !== -1) { + return 'dow'; + } else if (klass.indexOf('year') !== -1) { + return 'year'; + } + } + + /** + * Adds a value to the specified mode. + * + * @param {string} value - The value to add. + * @param {string} mode - The mode to add the value to. + */ + add(value, mode) { + this.value[mode].push(value.toString()); + } + + /** + * Removes a value from the specified mode. + * + * @param {string} value - The value to remove. + * @param {string} mode - The mode to remove the value from. + */ + remove(value, mode) { + let index = this.value[mode].indexOf(value.toString()); + if (index > -1) { + this.value[mode].splice(index, 1); + } + } + + /** + * Checks if a value exists in the specified mode. + * + * @param {string} value - The value to check for. + * @param {string} mode - The mode to check against. + * @returns {boolean} - True if the value exists, otherwise false. + */ + has(value, mode) { + return this.value[mode].indexOf(value.toString()) > -1; + } + + /** + * Parses a cron expression string and updates the widget values. + * + * @param {string} value - The cron expression to parse. + */ + parse(value) { + value = value.split(' '); + if (value.length === 5) { + // year is optional + value.push('*'); + } + this.parse_part(value[0].trim(), 'minute'); + this.parse_part(value[1].trim(), 'hour'); + this.parse_part(value[2].trim(), 'dom'); + this.parse_part(value[3].trim(), 'month'); + this.parse_part(value[4].trim(), 'dow'); + this.parse_part(value[5].trim(), 'year'); + } + + /** + * Parses a specific part of the cron expression and updates the + * corresponding mode values. + * + * @param {string} value - The value to parse for the mode. + * @param {string} mode - The mode to update with the parsed values. + */ + parse_part(value, mode) { + if (typeof value === 'string') { + value = value.split(','); + } + this.value[mode] = []; + let cnt; + if (value[0] === '*') { + let start, end; + if (mode === 'minute') { + start = 0; end = 59; + } else if (mode === 'hour') { + start = 0; end = 23; + } else if (mode === 'dom') { + start = 1; end = 31; + } else if (mode === 'month') { + start = 1; end = 12; + } else if (mode === 'dow') { + start = 0; end = 6; + } else if (mode === 'year') { + start = this.start_year; + end = this.end_year; + } + for (cnt=start; cnt < end + 1; cnt++) { + this.value[mode].push(cnt.toString()); + } + } else { + let val; + for (cnt=0; cnt < value.length; cnt++) { + val = value[cnt]; + if (val === '') { + continue; + } + val = parseInt(val, 10).toString(); + this.value[mode].push(val); + } + } + } + + /** + * Serializes the values of a specified mode into a cron expression format. + * + * @param {string} mode - The mode to serialize. + * @returns {string} - The serialized part for the mode. + */ + serialize(mode) { + let vals = this.value[mode]; + vals.sort(function(a, b) { + // int-sort - otherwise it's a lexical sort. + return parseInt(a, 10) - parseInt(b, 10); + }); + let maxlength = this.maxlengths()[mode]; + if (vals.length >= maxlength) { + return '*'; + } else { + return vals.join(','); + } + } + + /** + * Parses values from an input element. + * + * @param {jQuery} input - The input element to parse values from. + */ + parse_from_input(input) { + this.parse_part( + input.val(), + this.get_mode(this.get_edit_section(input)) + ); + } + + /** + * Serializes the current values into hidden input elements. + */ + serialize_to_input() { + let container = this.edit_area, + mode = this.get_mode(container), + input = $('.cron-value.' + mode + ' input', this.root); + input.val(this.serialize(mode)); + } + + /** + * Groups consecutive values in an array into sub-arrays. + * + * @param {Array} arr - The array of values to group. + * @returns {Array} - An array of grouped value arrays. + */ + group_value(arr) { + let groups = [], + group = [], + idx, + nidx; + for (idx=0; idx < arr.length; idx++) { + nidx = idx + 1; + if (idx === arr.length - 1) { + group.push(arr[idx]); + groups.push(group); + } else if (parseInt(arr[idx]) + 1 === parseInt(arr[nidx])) { + group.push(arr[idx]); + } else { + group.push(arr[idx]); + groups.push(group); + group = []; + } + } + return groups; + } + + /** + * Updates the summary display in the widget. + */ + update_summary() { + $('.crontab_summary .summary', this.root).html(this.summarize()); + } + + /** + * Summarizes and formats the current values. + * + * @returns {string} - HTML string containing the summary of values. + */ + summarize() { + const no_values = Object.values(this.value).every(v => Array.isArray(v) && v.length === 0); + if (no_values) { + return `
${this.translate('no_times_selected')}
` + } + + return [ + this.format_part( + 'minute', 'no_minutes_selected', + 'selected_minutes','all_minutes_selected' + ), + this.format_part( + 'hour', 'no_hours_selected', + 'selected_hours', 'all_hours_selected' + ), + this.format_part( + 'dom', 'no_dom_selected', + 'selected_dom', 'all_dom_selected' + ), + this.format_part( + 'month', 'no_month_selected', + 'selected_month', 'all_month_selected', + this.monthmap() + ), + this.format_part( + 'dow', 'no_dow_selected', + 'selected_dow', 'all_dow_selected', + this.dowmap() + ), + this.format_part( + 'year', 'no_year_selected', + 'selected_years', 'all_years_selected' + ) + ].join(''); + } + + /** + * Formats a specific part of the summary display. + * + * @param {string} value_name - The name of the value category. + * @param {string} no_values_selected - Translation key for no values selected message. + * @param {string} values_selected - Translation key for selected values message. + * @param {string} all_values_selected - Translation key for all values selected message. + * @param {Object} [value_map] - An optional map for displaying value names. + * @returns {string} - The formatted HTML string for the value part. + */ + format_part(value_name, no_values_selected, values_selected, + all_values_selected, value_map) { + let value = this.value[value_name], + value_len = value.length, + is_msg = false, + max_len = this.maxlengths()[value_name], + ret, + ret_values = ''; + if (value_len === 0) { + ret = this.translate(values_selected); + ret_values = this.translate(no_values_selected); + is_msg = true; + } else if (value_len < max_len) { + ret = this.translate(values_selected); + ret_values = this.format_groups( + this.group_value(value), + value_map + ); + } else { + ret = this.translate(values_selected); + ret_values = this.translate(all_values_selected); + } + return `
` + + `
${ret}
` + + `${ret_values}` + + '
'; + } + + /** + * Formats groups of values into a display string. + * + * @param {Array} groups - The grouped values to format. + * @param {Object} value_map - A mapping for displaying value names. + * @returns {string} - The formatted string for the groups. + */ + format_groups(groups, value_map) { + let ret = '', + idx, + group; + for (idx=0; idx < groups.length; idx++) { + group = groups[idx]; + if (group.length === 1) { + ret += this.display_value(group[0], value_map); + } else { + ret += this.display_value(group[0], value_map); + ret += '-'; + ret += this.display_value( + group[group.length - 1], + value_map + ); + } + if (idx !== groups.length - 1) { + ret += ', '; + } + } + return ret; + } + + /** + * Displays the value using the value map or returns the original value. + * + * @param {string} value - The value to display. + * @param {Object} [value_map] - An optional map for displaying value names. + * @returns {string} - The display string for the value. + */ + display_value(value, value_map) { + if (value_map) { + return value_map[value]; + } + return value; + } +}; + +////////////////////////////////////////////////////////////////////////////// +// yafowil.widget.array integration +////////////////////////////////////////////////////////////////////////////// + +/** + * Re-initializes widget on array add event. + */ +export function cron_on_array_add(inst, context) { + CronWidget.initialize(context); +} + +/** + * Registers subscribers to yafowil array events. + */ +export function register_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', cron_on_array_add); +} diff --git a/js/src/default/bundle.js b/js/src/default/bundle.js new file mode 100644 index 0000000..5879f4e --- /dev/null +++ b/js/src/default/bundle.js @@ -0,0 +1,18 @@ +import $ from 'jquery'; + +import {CronWidget} from './widget.js'; +import {register_array_subscribers} from './widget.js'; + +export * from './i18n.js'; +export * from './widget.js'; + +$(function() { + if (window.ts !== undefined) { + ts.ajax.register(CronWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(CronWidget.initialize, true); + } else { + CronWidget.initialize(); + } + register_array_subscribers(); +}); diff --git a/js/src/default/i18n.js b/js/src/default/i18n.js new file mode 100644 index 0000000..1ec58f5 --- /dev/null +++ b/js/src/default/i18n.js @@ -0,0 +1,106 @@ +export let i18n = { + en: { + select_minutes: 'Select Minutes', + select_hours: 'Select Hours', + select_dom: 'Select Day of Month', + select_month: 'Select Month', + select_dow: 'Select Day of Week', + select_year: 'Select Year', + select_all: 'Select All', + monthmap: { + 1: 'January', + 2: 'February', + 3: 'March', + 4: 'April', + 5: 'May', + 6: 'June', + 7: 'July', + 8: 'August', + 9: 'September', + 10: 'October', + 11: 'November', + 12: 'December' + }, + dowmap: { + 1: 'Monday', + 2: 'Tuesday', + 3: 'Wednesday', + 4: 'Thursday', + 5: 'Friday', + 6: 'Saturday', + 0: 'Sunday' + }, + summary: 'Summary', + no_times_selected: 'No times selected', + no_minutes_selected: 'No minutes selected', + selected_minutes: 'Minutes: ', + all_minutes_selected: 'Every minute', + no_hours_selected: 'No hour selected', + selected_hours: 'Hours: ', + all_hours_selected: 'Every hour', + no_dom_selected: 'No day of month selected', + selected_dom: 'Days of month: ', + all_dom_selected: 'Every day of month', + no_month_selected: 'No month selected', + selected_month: 'Month: ', + all_month_selected: 'Every month', + no_dow_selected: 'No day of week selected', + selected_dow: 'Days of week: ', + all_dow_selected: 'Every day of week', + no_year_selected: 'No year selected', + selected_years: 'Years: ', + all_years_selected: 'Every year' + }, + de: { + select_minutes: 'Minuten auswählen', + select_hours: 'Stunden auswählen', + select_dom: 'Monatstage auswählen', + select_month: 'Monate auswählen', + select_dow: 'Wochentage auswählen', + select_year: 'Jahre auswählen', + select_all: 'Alle auswählen', + monthmap: { + 1: 'Jänner', + 2: 'Feber', + 3: 'März', + 4: 'April', + 5: 'Mai', + 6: 'Juni', + 7: 'Juli', + 8: 'August', + 9: 'September', + 10: 'Oktober', + 11: 'November', + 12: 'Dezember' + }, + dowmap: { + 1: 'Montag', + 2: 'Dienstag', + 3: 'Mittwoch', + 4: 'Donnerstag', + 5: 'Freitag', + 6: 'Samstag', + 0: 'Sonntag' + }, + summary: 'Zusammenfassung', + no_times_selected: 'Keine Zeiten ausgewählt', + no_minutes_selected: 'Keine Minuten ausgewählt', + selected_minutes: 'Minuten: ', + all_minutes_selected: 'Jede Minute', + no_hours_selected: 'Keine Stunden ausgewählt', + selected_hours: 'Stunden: ', + all_hours_selected: 'Jede Stunde', + no_dom_selected: 'Keine Monatstage ausgewählt', + selected_dom: 'Monatstage: ', + all_dom_selected: 'Alle Monatstage', + no_month_selected: 'Keine Monate ausgewählt', + selected_month: 'Monate: ', + all_month_selected: 'Jedes Monat', + no_dow_selected: 'Keine Wochentage ausgewählt', + selected_dow: 'Wochentage: ', + all_dow_selected: 'Jeder Wochentag', + no_year_selected: 'Kein Jahr ausgewählt', + selected_years: 'Jahre: ', + all_years_selected: 'Jedes Jahr' + } +} diff --git a/js/src/default/widget.js b/js/src/default/widget.js new file mode 100644 index 0000000..3b5d300 --- /dev/null +++ b/js/src/default/widget.js @@ -0,0 +1,480 @@ +import $ from 'jquery'; +import {i18n} from './i18n.js'; + +export class CronWidget { + + static initialize(context) { + $('.crontab.widget', context).each(function () { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new CronWidget(elem, 'edit'); + }); + $('.display-crontab.widget', context).each(function () { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new CronWidget(elem, 'display'); + }); + } + + constructor(root, mode) { + root.data('yafowil-cron', this); + this.root = root; + this.mode = mode; + + let lang = root.data('lang'); + this.lang = lang ? lang : 'en'; + + let start_year = root.data('start_year'); + this.start_year = start_year ? start_year : new Date().getFullYear(); + + let end_year = root.data('end_year'); + this.end_year = end_year ? end_year : new Date().getFullYear() + 9; + + this.pressed = false; + + this.value = { + minute: [], + hour: [], + dom: [], + month: [], + dow: [], + year: [] + }; + + let summary_container_template = + '
' + + '' + this.translate('summary') + '' + + '

' + + '
'; + + if (mode === 'display') { + this.parse($('code', root).text()); + root.append($(summary_container_template)); + this.update_summary(); + return; + } + + let that = this; + + $('input[type="hidden"]', root).each(function () { + that.parse_from_input($(this)); + }); + + root.append($(summary_container_template)); + this.update_summary(); + + this.edit_area = $('.editarea', root); + this.editarea_cls = this.edit_area.attr('class'); + + this.edit_area.on('mousedown touchstart', function (evt) { + that.pressed = true; + }); + + $(document).on('mouseup touchend', function (evt) { + that.pressed = false; + }); + + $('button.edit', root).on('click', function(evt) { + evt.preventDefault(); + that.show_edit_section($(this)); + }); + } + + show_edit_section(trigger) { + let cnt, + edit_area = this.edit_area, + container = this.get_edit_section(trigger), + mode = this.get_mode(container); + if (edit_area.is(':visible') && edit_area.hasClass(mode)) { + container.removeClass('active'); + edit_area.attr('class', this.editarea_cls).html('').hide(); + return; + } + let header = $('

'), + content = $('
'); + if (mode === 'minute') { + header.text(this.translate('select_minutes')); + for (cnt=0; cnt <= 59; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } else if (mode === 'hour') { + header.text(this.translate('select_hours')); + let hour; + for (cnt=1; cnt <= 24; cnt++) { + // "0" should be rendered as last element. + hour = cnt < 24 ? cnt : 0; + content.append(this.make_button(hour, hour, mode)); + } + } else if (mode === 'dom') { + header.text(this.translate('select_dom')); + for (cnt=1; cnt <= 31; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } else if (mode === 'month') { + header.text(this.translate('select_month')); + let monthmap = this.monthmap(); + for (cnt=1; cnt <= 12; cnt++) { + content.append(this.make_button(cnt, monthmap[cnt], mode)); + } + } else if (mode === 'dow') { + header.text(this.translate('select_dow')); + let dowmap = this.dowmap(), + dow; + for (cnt=1; cnt <= 7; cnt++) { + // "0" should be rendered as last element. + dow = cnt < 7 ? cnt : 0; + content.append(this.make_button(dow, dowmap[dow], mode)); + } + } else if (mode === 'year') { + header.text(this.translate('select_year')); + for (cnt=this.start_year; cnt <= this.end_year; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } + header = header.add(this.make_button_all(mode)); + content = header.add(content); + edit_area.html(content).attr('class', this.editarea_cls + ' ' + mode).show(); + container.addClass('active'); + } + + make_button_all(mode) { + let button = $( + '' + ); + if (this.value[mode].length >= this.maxlengths()[mode]) { + button.addClass('active'); + } + let that = this; + button.on('click', function(evt) { + evt.preventDefault(); + let $this = $(this); + if ($this.hasClass('active')) { + // clear + $this.parent().find('.editcontainer button').each(function () { + $(this).removeClass('active'); + }); + $this.removeClass('active'); + that.parse_part('', mode); + } else { + // select all + $this.parent().find('.editcontainer button').each(function () { + $(this).removeClass('active').addClass('active'); + }); + $this.removeClass('active').addClass('active'); + that.parse_part('*', mode); + } + that.serialize_to_input(); + that.update_summary(); + }); + return button; + } + + make_button(value, name, mode) { + let button = $( + '' + ); + if (this.has(value, mode)) { + button.addClass('active'); + } + let that = this; + + let handler = function(evt) { + evt.preventDefault(); + let elem = $(this); + let container = that.edit_area; + if (elem.hasClass('active')) { + that.remove(elem.attr('name'), that.get_mode(container)); + that.serialize_to_input(); + elem.removeClass('active'); + } else { + that.add(elem.attr('name'), that.get_mode(container)); + that.serialize_to_input(); + elem.addClass('active'); + } + that.update_summary(); + }; + + button.on('click', function (evt) { + evt.preventDefault(); + }).on('mousedown touchstart', + handler + ).on('mouseenter touchenter', function (evt) { + if (evt.shiftKey === false && that.pressed === false) { + return; + } + handler.bind(this)(evt); + }).on('mouseup touchend', function (evt) { + that.pressed = false; + }); + return button; + } + + maxlengths() { + return { + minute: 60, + hour: 24, + dom: 31, + month: 12, + dow: 7, + year: this.end_year - this.start_year + 1 + }; + } + + translate(msg) { + return i18n[this.lang][msg]; + } + + monthmap() { + return i18n[this.lang].monthmap; + } + + dowmap() { + return i18n[this.lang].dowmap; + } + + get_edit_section(elem) { + return elem.closest('.cron-value'); + } + + get_mode(elem) { + let klass = elem.attr('class'); + if (klass.indexOf('minute') !== -1) { + return 'minute'; + } else if (klass.indexOf('hour') !== -1) { + return 'hour'; + } else if (klass.indexOf('dom') !== -1) { + return 'dom'; + } else if (klass.indexOf('month') !== -1) { + return 'month'; + } else if (klass.indexOf('dow') !== -1) { + return 'dow'; + } else if (klass.indexOf('year') !== -1) { + return 'year'; + } + } + + add(value, mode) { + this.value[mode].push(value.toString()); + } + + remove(value, mode) { + let index = this.value[mode].indexOf(value.toString()); + if (index > -1) { + this.value[mode].splice(index, 1); + } + } + + has(value, mode) { + return this.value[mode].indexOf(value.toString()) > -1; + } + + parse(value) { + value = value.split(' '); + if (value.length === 5) { + // year is optional + value.push('*'); + } + this.parse_part(value[0].trim(), 'minute'); + this.parse_part(value[1].trim(), 'hour'); + this.parse_part(value[2].trim(), 'dom'); + this.parse_part(value[3].trim(), 'month'); + this.parse_part(value[4].trim(), 'dow'); + this.parse_part(value[5].trim(), 'year'); + } + + parse_part(value, mode) { + if (typeof value === 'string') { + value = value.split(','); + } + this.value[mode] = []; + let cnt; + if (value[0] === '*') { + let start, end; + if (mode === 'minute') { + start = 0; end = 59; + } else if (mode === 'hour') { + start = 0; end = 23; + } else if (mode === 'dom') { + start = 1; end = 31; + } else if (mode === 'month') { + start = 1; end = 12; + } else if (mode === 'dow') { + start = 0; end = 6; + } else if (mode === 'year') { + start = this.start_year; + end = this.end_year; + } + for (cnt=start; cnt < end + 1; cnt++) { + this.value[mode].push(cnt.toString()); + } + } else { + let val; + for (cnt=0; cnt < value.length; cnt++) { + val = value[cnt]; + if (val === '') { + continue; + } + val = parseInt(val, 10).toString(); + this.value[mode].push(val); + } + } + } + + serialize(mode) { + let vals = this.value[mode]; + vals.sort(function(a, b) { + // int-sort - otherwise it's a lexical sort. + return parseInt(a, 10) - parseInt(b, 10); + }); + let maxlength = this.maxlengths()[mode]; + if (vals.length >= maxlength) { + return '*'; + } else { + return vals.join(','); + } + } + + parse_from_input(input) { + this.parse_part( + input.val(), + this.get_mode(this.get_edit_section(input)) + ); + } + + serialize_to_input() { + let container = this.edit_area, + mode = this.get_mode(container), + input = $('.cron-value.' + mode + ' input', this.root); + input.val(this.serialize(mode)); + } + + group_value(arr) { + let groups = [], + group = [], + idx, + nidx; + for (idx=0; idx < arr.length; idx++) { + nidx = idx + 1; + if (idx === arr.length - 1) { + group.push(arr[idx]); + groups.push(group); + } else if (parseInt(arr[idx]) + 1 === parseInt(arr[nidx])) { + group.push(arr[idx]); + } else { + group.push(arr[idx]); + groups.push(group); + group = []; + } + } + return groups; + } + + update_summary() { + $('.crontab_summary .summary', this.root).html(this.summarize()); + } + + summarize() { + return [ + this.format_part( + 'minute', 'no_minutes_selected', + 'selected_minutes','all_minutes_selected' + ), + this.format_part( + 'hour', 'no_hours_selected', + 'selected_hours', 'all_hours_selected' + ), + this.format_part( + 'dom', 'no_dom_selected', + 'selected_dom', 'all_dom_selected' + ), + this.format_part( + 'month', 'no_month_selected', + 'selected_month', 'all_month_selected', + this.monthmap() + ), + this.format_part( + 'dow', 'no_dow_selected', + 'selected_dow', 'all_dow_selected', + this.dowmap() + ), + this.format_part( + 'year', 'no_year_selected', + 'selected_years', 'all_years_selected' + ) + ].join('
'); + } + + format_part(value_name, no_values_selected, values_selected, + all_values_selected, value_map) { + let value = this.value[value_name], + value_len = value.length, + max_len = this.maxlengths()[value_name], + ret; + if (value_len === 0) { + ret = this.translate(no_values_selected); + } else if (value_len < max_len) { + ret = this.translate(values_selected) + this.format_groups( + this.group_value(value), + value_map + ); + } else { + ret = this.translate(all_values_selected); + } + return ret; + } + + format_groups(groups, value_map) { + let ret = '', + idx, + group; + for (idx=0; idx < groups.length; idx++) { + group = groups[idx]; + if (group.length === 1) { + ret += this.display_value(group[0], value_map); + } else { + ret += this.display_value(group[0], value_map); + ret += '-'; + ret += this.display_value( + group[group.length - 1], + value_map + ); + } + if (idx !== groups.length - 1) { + ret += ', '; + } + } + return ret; + } + + display_value(value, value_map) { + if (value_map) { + return value_map[value]; + } + return value; + } +}; + +////////////////////////////////////////////////////////////////////////////// +// yafowil.widget.array integration +////////////////////////////////////////////////////////////////////////////// + +export function cron_on_array_add(inst, context) { + CronWidget.initialize(context); +} + +export function register_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', cron_on_array_add); +} diff --git a/js/tests/test_cron.js b/js/tests/test_cron.js new file mode 100644 index 0000000..f91e795 --- /dev/null +++ b/js/tests/test_cron.js @@ -0,0 +1,58 @@ +import {CronWidget} from "../src/default/widget"; +import {register_array_subscribers} from "../src/default/widget"; +import $ from 'jquery'; + +QUnit.test('initialize', assert => { + let el = $('
').addClass('crontab').addClass('widget'); + let widget = new CronWidget(el, 'edit'); + assert.ok(widget); + + el.remove(); + widget = null; +}); + +QUnit.test('register_array_subscribers', assert => { + let _array_subscribers = { + on_add: [] + }; + + // window.yafowil_array is undefined - return + register_array_subscribers(); + assert.deepEqual(_array_subscribers['on_add'], []); + + // patch yafowil_array + window.yafowil_array = { + on_array_event: function(evt_name, evt_function) { + _array_subscribers[evt_name] = evt_function; + }, + inside_template(elem) { + return elem.parents('.arraytemplate').length > 0; + } + }; + register_array_subscribers(); + + // create table DOM + let table = $('') + .append($('')) + .append($('
')) + .appendTo('body'); + + let el = $(`
`).addClass('crontab widget'); + $('td', table).addClass('arraytemplate'); + el.appendTo($('td', table)); + + // invoke array on_add - returns + _array_subscribers['on_add'].apply(null, $('tr', table)); + let widget = el.data('yafowil-cron'); + assert.notOk(widget); + $('td', table).removeClass('arraytemplate'); + + // invoke array on_add + el.attr('id', ''); + _array_subscribers['on_add'].apply(null, $('tr', table)); + widget = el.data('yafowil-cron'); + assert.ok(widget); + table.remove(); + window.yafowil_array = undefined; + _array_subscribers = undefined; +}); \ No newline at end of file diff --git a/js/wtr.config.mjs b/js/wtr.config.mjs new file mode 100644 index 0000000..f333d83 --- /dev/null +++ b/js/wtr.config.mjs @@ -0,0 +1,25 @@ +import {importMapsPlugin} from '@web/dev-server-import-maps'; + +export default { + nodeResolve: true, + testFramework: { + path: './node_modules/web-test-runner-qunit/dist/autorun.js', + config: { + noglobals: false + } + }, + files: [ + 'js/tests/**/test_*.js' + ], + plugins: [ + importMapsPlugin({ + inject: { + importMap: { + imports: { + 'jquery': './node_modules/jquery/dist-module/jquery.module.js' + }, + }, + }, + }), + ], +} diff --git a/mx.ini b/mx.ini new file mode 100644 index 0000000..cddfe3b --- /dev/null +++ b/mx.ini @@ -0,0 +1,97 @@ +[settings] +threads = 5 + +# conestack git URLs +cs = https://github.com/conestack +cs_push = git@github.com:conestack + +# checkout source packages +checkout_packages = true + +# feature branch to checkout +feature_branch = refactor-package-layout + +# main package +main-package = -e .[test] + +mxmake-test-runner = pytest +# fixed dependency package versions +version-overrides = + pyramid==2.0.2 + +# templates and environment +mxmake-templates = + run-tests + run-coverage + +mxmake-test-path = src +mxmake-source-path = src/yafowil/widget/cron + +[mxmake-env] +TESTRUN_MARKER = 1 + +[mxmake-run-tests] +environment = env + +[mxmake-run-coverage] +environment = env + +############################################################################### +# base packages +############################################################################### + +[odict] +use = ${settings:checkout_packages} +url = ${settings:cs}/odict.git +pushurl = ${settings:cs_push}/odict.git +branch = ${settings:feature_branch} +mxmake-test-path = tests +mxmake-source-path = src/odict + +[plumber] +use = ${settings:checkout_packages} +url = ${settings:cs}/plumber.git +pushurl = ${settings:cs_push}/plumber.git +branch = ${settings:feature_branch} +extras = test +mxmake-test-path = tests +mxmake-source-path = src/plumber + +############################################################################### +# node packages +############################################################################### + +[node] +use = ${settings:checkout_packages} +url = ${settings:cs}/node.git +pushurl = ${settings:cs_push}/node.git +branch = refactor-package-layout +mxmake-test-path = src +mxmake-source-path = src/node + + +############################################################################### +# yafowil packages +############################################################################### + +[yafowil] +use = ${settings:checkout_packages} +url = ${settings:cs}/yafowil.git +pushurl = ${settings:cs_push}/yafowil.git +branch = ${settings:feature_branch} +extras = test +mxmake-test-path = src +mxmake-source-path = src/yafowil + +############################################################################### +# cone packages +############################################################################### + +[webresource] +use = ${settings:checkout_packages} +url = ${settings:cs}/webresource.git +pushurl = ${settings:cs_push}/webresource.git +branch = master +extras = test +mxmake-test-path = tests +mxmake-source-path = webresource diff --git a/mxmake.yaml b/mxmake.yaml new file mode 100644 index 0000000..13f206a --- /dev/null +++ b/mxmake.yaml @@ -0,0 +1,30 @@ +topics: + core: + mxenv: + VENV_FOLDER: venv + PYTHON_PACKAGE_INSTALLER: uv + + qa: + test: + TEST_COMMAND: $(VENV_FOLDER)/bin/pytest src/yafowil/widget/cron/tests + + coverage: + COVERAGE_COMMAND: | + \ + $(VENV_FOLDER)/bin/coverage run \ + --omit src/yafowil/widget/cron/example.py \ + --source src/yafowil/widget/cron \ + -m pytest src/yafowil/widget/cron/tests \ + && $(VENV_FOLDER)/bin/coverage report --fail-under=99 + + js: + nodejs: + NODEJS_PACKAGE_MANAGER: pnpm + + rollup: + ROLLUP_CONFIG: js/rollup.conf.js + + wtr: + WTR_CONFIG: js/wtr.config.mjs + +mx-ini: true diff --git a/package.json b/package.json new file mode 100644 index 0000000..30b2964 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@web/dev-server-import-maps": "^0.2.1", + "@web/test-runner": "^0.18.3", + "@web/test-runner-core": "^0.13.2", + "install": "^0.13.0", + "jquery": "^4.0.0-beta.2", + "qunit": "^2.20.1", + "rollup": "^2.79.2", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-postcss": "^4.0.2", + "sass": "^1.80.4", + "web-test-runner-qunit": "^2.0.0" + }, + "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" + } + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d0beebc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,71 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "yafowil.widget.cron" +version = "2.0.0.dev0" +description = "Cron widget for YAFOWIL" +readme = "README.rst" +license = {text = "Simplified BSD"} +authors = [{name = "Yafowil Contributors", email = "dev@conestack.org"}] +classifiers = [ + "License :: OSI Approved :: BSD License", + "Environment :: Web Environment", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", +] +dependencies = [ + "crontab", + "yafowil>3.99", +] + +[project.optional-dependencies] +test = [ + "lxml", + "pytest", +] + +[project.entry-points."yafowil.plugin"] +register = "yafowil.widget.cron:register" +example = "yafowil.widget.cron.example:get_example" + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/x-rst" +fragments = [ + {path = "README.rst"}, + {text = "\n\n"}, + {path = "CHANGES.rst"}, + {text = "\n\n"}, + {path = "LICENSE.rst"}, +] + +[tool.hatch.build.targets.sdist] +exclude = [ + "/.github/", + "/js/", + "/Makefile", + "/mx.ini", + "/mxmake.yaml", + "/package.json", + "/pnpm-lock.yaml", + "/scss", + "/.jshintrc" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/yafowil"] + +[tool.pytest.ini_options] +consider_namespace_packages = true +addopts = ["--import-mode=importlib"] +pythonpath = "src" + +[tool.zest-releaser] +create-wheel = true diff --git a/scss/default/widget.scss b/scss/default/widget.scss new file mode 100644 index 0000000..71f3568 --- /dev/null +++ b/scss/default/widget.scss @@ -0,0 +1,127 @@ +.clearfix::after { + content: ""; + display: table; + clear: both; +} + +.crontab.widget { + .cron-value { + display: inline-block; + margin-right: 2px; + } + .crontab_summary { + margin-top: 1em; + } + .editarea { + display: none; + width: 100%; + + &.minute { + .editcontainer { + button { + width: 3em; + } + button:nth-child(11), + button:nth-child(21), + button:nth-child(31), + button:nth-child(41), + button:nth-child(51) { + clear: left; + } + } + } + + &.hour { + .editcontainer { + button { + width: 3em; + } + button:nth-child(13) { + clear: left; + } + } + } + + &.dow { + .editcontainer { + button { + min-width: 5em; + } + } + } + + &.dom { + .editcontainer { + button { + width: 3em; + } + button:nth-child(8), + button:nth-child(15), + button:nth-child(22), + button:nth-child(29) { + clear: left; + } + } + } + + &.month { + .editcontainer { + button { + min-width: 7em; + } + button:nth-child(4), + button:nth-child(7), + button:nth-child(10) { + clear: left; + } + } + } + + &.year { + .editcontainer { + button { + min-width: 4em; + } + button:nth-child(11), + button:nth-child(21), + button:nth-child(31), + button:nth-child(41), + button:nth-child(51), + button:nth-child(61), + button:nth-child(71), + button:nth-child(81), + button:nth-child(91) { + clear: left; + } + } + } + + .active { + background-color: var(--yafowil-accent-color, #0d6efd); + border-color: var(--yafowil-accent-color, #0d6efd); + color: var(--yafowil-accent-font-color, #fff); + } + .select_all { + margin: 1em 0; + } + .editcontainer { + &::after { + content: ""; + display: table; + clear: both; + } + button { + display: block; + border-radius: 2px; + float: left; + margin: 0 2px 2px 0; + padding: 0.4em; + + &:last-child::after { + clear: both; + } + } + } + } +} + diff --git a/setup.py b/setup.py deleted file mode 100644 index a2abd5e..0000000 --- a/setup.py +++ /dev/null @@ -1,62 +0,0 @@ -from setuptools import find_packages -from setuptools import setup -import os - - -def read_file(name): - with open(os.path.join(os.path.dirname(__file__), name)) as f: - return f.read() - - -version = '1.3.dev0' -shortdesc = 'Cron widget for YAFOWIL' -longdesc = '\n\n'.join([read_file(name) for name in [ - 'README.rst', - 'CHANGES.rst', - 'LICENSE.rst' -]]) -tests_require = ['yafowil[test]'] - - -setup( - name='yafowil.widget.cron', - version=version, - description=shortdesc, - long_description=longdesc, - classifiers=[ - 'License :: OSI Approved :: BSD License', - 'Environment :: Web Environment', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - ], - keywords='', - author='Yafowil Contributors', - author_email='dev@conestack.org', - url=u'http://github.com/conestack/yafowil.widget.cron', - license='Simplified BSD', - packages=find_packages('src'), - package_dir={'': 'src'}, - namespace_packages=['yafowil', 'yafowil.widget'], - include_package_data=True, - zip_safe=False, - install_requires=[ - 'setuptools', - 'crontab', - 'yafowil>2.2', - ], - tests_require=tests_require, - extras_require=dict( - test=tests_require, - ), - test_suite="yafowil.widget.cron.tests", - entry_points=""" - [yafowil.plugin] - register = yafowil.widget.cron:register - example = yafowil.widget.cron.example:get_example - """ -) diff --git a/src/yafowil/__init__.py b/src/yafowil/__init__.py deleted file mode 100644 index de40ea7..0000000 --- a/src/yafowil/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/src/yafowil/widget/__init__.py b/src/yafowil/widget/__init__.py deleted file mode 100644 index de40ea7..0000000 --- a/src/yafowil/widget/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__import__('pkg_resources').declare_namespace(__name__) diff --git a/src/yafowil/widget/cron/__init__.py b/src/yafowil/widget/cron/__init__.py index a59315d..405325f 100644 --- a/src/yafowil/widget/cron/__init__.py +++ b/src/yafowil/widget/cron/__init__.py @@ -1,23 +1,118 @@ from yafowil.base import factory from yafowil.utils import entry_point import os +import webresource as wr -resourcedir = os.path.join(os.path.dirname(__file__), 'resources') +resources_dir = os.path.join(os.path.dirname(__file__), 'resources') + + +############################################################################## +# Default +############################################################################## + +# webresource ################################################################ + +resources = wr.ResourceGroup( + name='yafowil.widget.cron', + directory=resources_dir, + path='yafowil-cron' +) +resources.add(wr.ScriptResource( + name='yafowil-cron-js', + directory=os.path.join(resources_dir, 'default'), + path='yafowil-cron/default', + depends='jquery-js', + resource='widget.js', + compressed='widget.min.js' +)) +resources.add(wr.StyleResource( + name='yafowil-cron-css', + directory=os.path.join(resources_dir, 'default'), + path='yafowil-cron/default', + resource='widget.min.css' +)) + +# B/C resources ############################################################## + js = [{ 'group': 'yafowil.widget.cron.common', - 'resource': 'widget.js', + 'resource': 'default/widget.js', 'order': 21, }] -default_css = [{ +css = [{ + 'group': 'yafowil.widget.cron.common', + 'resource': 'default/widget.min.css', + 'order': 21, +}] + + +############################################################################## +# Bootstrap 5 +############################################################################## + +# webresource ################################################################ + +bootstrap5_resources = wr.ResourceGroup( + name='yafowil.widget.cron', + directory=resources_dir, + path='yafowil-cron' +) +bootstrap5_resources.add(wr.ScriptResource( + name='yafowil-cron-js', + directory=os.path.join(resources_dir, 'bootstrap5'), + path='yafowil-cron/bootstrap5', + depends='jquery-js', + resource='widget.js', + compressed='widget.min.js' +)) +bootstrap5_resources.add(wr.StyleResource( + name='yafowil-cron-css', + directory=os.path.join(resources_dir, 'default'), + path='yafowil-cron/default', + resource='widget.min.css' +)) + +# B/C resources ############################################################## + +bootstrap5_js = [{ 'group': 'yafowil.widget.cron.common', - 'resource': 'widget.css', + 'resource': 'bootstrap5/widget.js', 'order': 21, }] +############################################################################## +# Registration +############################################################################## + @entry_point(order=10) def register(): from yafowil.widget.cron import widget # noqa - factory.register_theme('default', 'yafowil.widget.cron', - resourcedir, js=js, css=default_css) + + widget_name = 'yafowil.widget.cron' + + # Default + factory.register_theme( + 'default', + widget_name, + resources_dir, + js=js, + css=css + ) + factory.register_resources('default', widget_name, resources) + + # Bootstrap 5 + factory.register_theme( + ['bootstrap5'], + widget_name, + resources_dir, + js=bootstrap5_js, + css=css + ) + + factory.register_resources( + ['bootstrap5'], + widget_name, + bootstrap5_resources + ) diff --git a/src/yafowil/widget/cron/example.py b/src/yafowil/widget/cron/example.py index 4fbb019..f54547e 100644 --- a/src/yafowil/widget/cron/example.py +++ b/src/yafowil/widget/cron/example.py @@ -60,8 +60,48 @@ def cron_example2(): } +DOC_CRON_DISPLAY = """ +Display Mode +------------ + +The widget's display mode renders only the widget summary. + +The wrapper div can receive additional classes via the ``display_class`` +widget attribute. + +.. code-block:: python + + value = '0,30 0,6,12,18 1,15,30 3,12 1,2,3,4,5 2025' + cron = factory('cron', name='cronwidget', mode='display', value=value, props={ + # 'display_class': 'my_additional_class' + }) +""" + + +def cron_example_display(): + value = '0,30 0,6,12,18 1,15,30 3,12 1,2,3,4,5 2025' + part = factory(u'fieldset', name='yafowil.widget.cron') + part['cron'] = factory( + '#field:cron', + value=value, + mode='display', + props={ + 'leaf': True, + 'label': 'Cron Widget', + 'lang': 'en', + 'start_year': '2000', + 'end_year': '2099', + }) + return { + 'widget': part, + 'doc': DOC_CRON_DISPLAY, + 'title': 'Cron', + } + + def get_example(): return [ cron_example(), # cron_example2(), + cron_example_display() ] diff --git a/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.mo b/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.mo index 6b65ed9..be5cbdb 100644 Binary files a/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.mo and b/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.mo differ diff --git a/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.po b/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.po index f80e3bf..284f2a5 100644 --- a/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.po +++ b/src/yafowil/widget/cron/locales/de/LC_MESSAGES/yafowil.widget.cron.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2017-05-10 13:40+0200\n" +"POT-Creation-Date: 2025-03-21 10:47+0100\n" "PO-Revision-Date: 2017-05-10 13:37+0200\n" "Last-Translator: Robert Niederreiter \n" "Language-Team: German\n" @@ -15,32 +15,37 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: src/yafowil/widget/cron/widget.py:61 +msgid "invalid_cron_rule" +msgstr "" +"Ungültige Cron-Regel. Wählen Sie mindestens ein Element für jedes Kriterium." + #. Default: Minute -#: src/yafowil/widget/cron/widget.py:78 +#: src/yafowil/widget/cron/widget.py:123 msgid "label_minute" msgstr "Minute" #. Default: Hour -#: src/yafowil/widget/cron/widget.py:84 +#: src/yafowil/widget/cron/widget.py:131 msgid "label_hour" msgstr "Stunde" #. Default: Day of Month -#: src/yafowil/widget/cron/widget.py:90 +#: src/yafowil/widget/cron/widget.py:139 msgid "label_dom" msgstr "Monatstag" #. Default: Month -#: src/yafowil/widget/cron/widget.py:96 +#: src/yafowil/widget/cron/widget.py:147 msgid "label_month" msgstr "Monat" #. Default: Day of Week -#: src/yafowil/widget/cron/widget.py:102 +#: src/yafowil/widget/cron/widget.py:155 msgid "label_dow" msgstr "Wochentag" #. Default: Year -#: src/yafowil/widget/cron/widget.py:108 +#: src/yafowil/widget/cron/widget.py:163 msgid "label_year" msgstr "Jahr" diff --git a/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.mo b/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.mo index fcea4e3..b609b6d 100644 Binary files a/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.mo and b/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.mo differ diff --git a/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.po b/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.po index ed54c96..44f7ee7 100644 --- a/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.po +++ b/src/yafowil/widget/cron/locales/en/LC_MESSAGES/yafowil.widget.cron.po @@ -5,7 +5,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2017-05-10 13:40+0200\n" +"POT-Creation-Date: 2025-03-21 10:47+0100\n" "PO-Revision-Date: 2017-05-10 13:37+0200\n" "Last-Translator: Robert Niederreiter \n" "Language-Team: English\n" @@ -15,32 +15,36 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: src/yafowil/widget/cron/widget.py:61 +msgid "invalid_cron_rule" +msgstr "Invalid cron rule. You must at least select one item for each criteria" + #. Default: Minute -#: src/yafowil/widget/cron/widget.py:78 +#: src/yafowil/widget/cron/widget.py:123 msgid "label_minute" msgstr "Minute" #. Default: Hour -#: src/yafowil/widget/cron/widget.py:84 +#: src/yafowil/widget/cron/widget.py:131 msgid "label_hour" msgstr "Hour" #. Default: Day of Month -#: src/yafowil/widget/cron/widget.py:90 +#: src/yafowil/widget/cron/widget.py:139 msgid "label_dom" msgstr "Day of Month" #. Default: Month -#: src/yafowil/widget/cron/widget.py:96 +#: src/yafowil/widget/cron/widget.py:147 msgid "label_month" msgstr "Month" #. Default: Day of Week -#: src/yafowil/widget/cron/widget.py:102 +#: src/yafowil/widget/cron/widget.py:155 msgid "label_dow" msgstr "Day of Week" #. Default: Year -#: src/yafowil/widget/cron/widget.py:108 +#: src/yafowil/widget/cron/widget.py:163 msgid "label_year" msgstr "Year" diff --git a/src/yafowil/widget/cron/locales/yafowil.widget.cron.pot b/src/yafowil/widget/cron/locales/yafowil.widget.cron.pot index f6d9f7c..b797e1d 100644 --- a/src/yafowil/widget/cron/locales/yafowil.widget.cron.pot +++ b/src/yafowil/widget/cron/locales/yafowil.widget.cron.pot @@ -1,47 +1,51 @@ # # SOME DESCRIPTIVE TITLE # This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , 2017. +# FIRST AUTHOR , 2025. #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE 1.0\n" -"POT-Creation-Date: 2017-05-10 13:40+0200\n" +"POT-Creation-Date: 2025-03-21 10:47+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Lingua 4.11\n" +"Generated-By: Lingua 4.15.0\n" + +#: ./src/yafowil/widget/cron/widget.py:61 +msgid "invalid_cron_rule" +msgstr "" #. Default: Minute -#: ./src/yafowil/widget/cron/widget.py:78 +#: ./src/yafowil/widget/cron/widget.py:123 msgid "label_minute" msgstr "" #. Default: Hour -#: ./src/yafowil/widget/cron/widget.py:84 +#: ./src/yafowil/widget/cron/widget.py:131 msgid "label_hour" msgstr "" #. Default: Day of Month -#: ./src/yafowil/widget/cron/widget.py:90 +#: ./src/yafowil/widget/cron/widget.py:139 msgid "label_dom" msgstr "" #. Default: Month -#: ./src/yafowil/widget/cron/widget.py:96 +#: ./src/yafowil/widget/cron/widget.py:147 msgid "label_month" msgstr "" #. Default: Day of Week -#: ./src/yafowil/widget/cron/widget.py:102 +#: ./src/yafowil/widget/cron/widget.py:155 msgid "label_dow" msgstr "" #. Default: Year -#: ./src/yafowil/widget/cron/widget.py:108 +#: ./src/yafowil/widget/cron/widget.py:163 msgid "label_year" msgstr "" diff --git a/src/yafowil/widget/cron/resources/bootstrap5/widget.js b/src/yafowil/widget/cron/resources/bootstrap5/widget.js new file mode 100644 index 0000000..34900e2 --- /dev/null +++ b/src/yafowil/widget/cron/resources/bootstrap5/widget.js @@ -0,0 +1,579 @@ +var yafowil_cron = (function (exports, $) { + 'use strict'; + + let i18n = { + en: { + select_minutes: 'Select Minutes', + select_hours: 'Select Hours', + select_dom: 'Select Day of Month', + select_month: 'Select Month', + select_dow: 'Select Day of Week', + select_year: 'Select Year', + select_all: 'Select All', + monthmap: { + 1: 'January', + 2: 'February', + 3: 'March', + 4: 'April', + 5: 'May', + 6: 'June', + 7: 'July', + 8: 'August', + 9: 'September', + 10: 'October', + 11: 'November', + 12: 'December' + }, + dowmap: { + 1: 'Monday', + 2: 'Tuesday', + 3: 'Wednesday', + 4: 'Thursday', + 5: 'Friday', + 6: 'Saturday', + 0: 'Sunday' + }, + summary: 'Summary', + no_times_selected: 'No times selected', + no_minutes_selected: 'No minutes selected', + selected_minutes: 'Minutes: ', + all_minutes_selected: 'Every minute', + no_hours_selected: 'No hour selected', + selected_hours: 'Hours: ', + all_hours_selected: 'Every hour', + no_dom_selected: 'No day of month selected', + selected_dom: 'Days of month: ', + all_dom_selected: 'Every day of month', + no_month_selected: 'No month selected', + selected_month: 'Month: ', + all_month_selected: 'Every month', + no_dow_selected: 'No day of week selected', + selected_dow: 'Days of week: ', + all_dow_selected: 'Every day of week', + no_year_selected: 'No year selected', + selected_years: 'Years: ', + all_years_selected: 'Every year' + }, + de: { + select_minutes: 'Minuten auswählen', + select_hours: 'Stunden auswählen', + select_dom: 'Monatstage auswählen', + select_month: 'Monate auswählen', + select_dow: 'Wochentage auswählen', + select_year: 'Jahre auswählen', + select_all: 'Alle auswählen', + monthmap: { + 1: 'Jänner', + 2: 'Feber', + 3: 'März', + 4: 'April', + 5: 'Mai', + 6: 'Juni', + 7: 'Juli', + 8: 'August', + 9: 'September', + 10: 'Oktober', + 11: 'November', + 12: 'Dezember' + }, + dowmap: { + 1: 'Montag', + 2: 'Dienstag', + 3: 'Mittwoch', + 4: 'Donnerstag', + 5: 'Freitag', + 6: 'Samstag', + 0: 'Sonntag' + }, + summary: 'Zusammenfassung', + no_times_selected: 'Keine Zeiten ausgewählt', + no_minutes_selected: 'Keine Minuten ausgewählt', + selected_minutes: 'Minuten: ', + all_minutes_selected: 'Jede Minute', + no_hours_selected: 'Keine Stunden ausgewählt', + selected_hours: 'Stunden: ', + all_hours_selected: 'Jede Stunde', + no_dom_selected: 'Keine Monatstage ausgewählt', + selected_dom: 'Monatstage: ', + all_dom_selected: 'Alle Monatstage', + no_month_selected: 'Keine Monate ausgewählt', + selected_month: 'Monate: ', + all_month_selected: 'Jedes Monat', + no_dow_selected: 'Keine Wochentage ausgewählt', + selected_dow: 'Wochentage: ', + all_dow_selected: 'Jeder Wochentag', + no_year_selected: 'Kein Jahr ausgewählt', + selected_years: 'Jahre: ', + all_years_selected: 'Jedes Jahr' + } + }; + + class CronWidget { + static initialize(context) { + $('.crontab.widget', context).each(function () { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new CronWidget(elem, 'edit'); + }); + $('.display-crontab.widget', context).each(function () { + let elem = $(this); + if (window.yafowil_array !== undefined && + window.yafowil_array.inside_template(elem)) { + return; + } + new CronWidget(elem, 'display'); + }); + } + constructor(root, mode) { + root.data('yafowil-cron', this); + this.root = root; + this.mode = mode; + let lang = root.data('lang'); + this.lang = lang ? lang : 'en'; + let start_year = root.data('start_year'); + this.start_year = start_year ? start_year : new Date().getFullYear(); + let end_year = root.data('end_year'); + this.end_year = end_year ? end_year : new Date().getFullYear() + 9; + this.pressed = false; + this.value = { + minute: [], + hour: [], + dom: [], + month: [], + dow: [], + year: [] + }; + let summary_container_template = + '
' + + '
' + + '
' + this.translate('summary') + '
' + + '

' + + '
' + + '
'; + if (mode === 'display') { + this.parse($('code', root).text()); + root.append($(summary_container_template)); + this.update_summary(); + return; + } + let that = this; + $('input[type="hidden"]', root).each(function () { + that.parse_from_input($(this)); + }); + root.append($(summary_container_template)); + this.update_summary(); + this.edit_area = $('.editarea', root); + this.editarea_cls = this.edit_area.attr('class'); + this.edit_area.on('mousedown touchstart', function (evt) { + that.pressed = true; + }); + $(document).on('mouseup touchend', function (evt) { + that.pressed = false; + }); + $('button.edit', root).on('click', function(evt) { + evt.preventDefault(); + $('button.edit', root).removeClass('active'); + that.show_edit_section($(this)); + }); + } + show_edit_section(trigger) { + trigger.addClass('active'); + let cnt, + edit_area = this.edit_area, + container = this.get_edit_section(trigger), + mode = this.get_mode(container); + if (edit_area.is(':visible') && edit_area.hasClass(mode)) { + container.removeClass('active'); + trigger.removeClass('active'); + trigger.blur(); + edit_area.attr('class', this.editarea_cls).html('').hide(); + return; + } + let header = $('

'), + content = $('
'); + if (mode === 'minute') { + header.text(this.translate('select_minutes')); + for (cnt=0; cnt <= 59; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } else if (mode === 'hour') { + header.text(this.translate('select_hours')); + let hour; + for (cnt=1; cnt <= 24; cnt++) { + hour = cnt < 24 ? cnt : 0; + content.append(this.make_button(hour, hour, mode)); + } + } else if (mode === 'dom') { + header.text(this.translate('select_dom')); + for (cnt=1; cnt <= 31; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } else if (mode === 'month') { + header.text(this.translate('select_month')); + let monthmap = this.monthmap(); + for (cnt=1; cnt <= 12; cnt++) { + content.append(this.make_button(cnt, monthmap[cnt], mode)); + } + } else if (mode === 'dow') { + header.text(this.translate('select_dow')); + let dowmap = this.dowmap(), + dow; + for (cnt=1; cnt <= 7; cnt++) { + dow = cnt < 7 ? cnt : 0; + content.append(this.make_button(dow, dowmap[dow], mode)); + } + } else if (mode === 'year') { + header.text(this.translate('select_year')); + for (cnt=this.start_year; cnt <= this.end_year; cnt++) { + content.append(this.make_button(cnt, cnt, mode)); + } + } + header = header.add(this.make_button_all(mode)); + content = header.add(content); + edit_area.html(content).attr('class', this.editarea_cls + ' ' + mode).show(); + container.addClass('active'); + } + make_button_all(mode) { + let button = $( + '' + ); + if (this.value[mode].length >= this.maxlengths()[mode]) { + button.addClass('active'); + } + let that = this; + button.on('click', function(evt) { + evt.preventDefault(); + let $this = $(this); + if ($this.hasClass('active')) { + $this.parent().find('.editcontainer button').each(function () { + $(this).removeClass('active'); + }); + $this.removeClass('active'); + that.parse_part('', mode); + } else { + $this.parent().find('.editcontainer button').each(function () { + $(this).removeClass('active').addClass('active'); + }); + $this.removeClass('active').addClass('active'); + that.parse_part('*', mode); + } + that.serialize_to_input(); + that.update_summary(); + }); + return button; + } + make_button(value, name, mode) { + let button = $( + '' + ); + if (this.has(value, mode)) { + button.addClass('active'); + } + let that = this; + let handler = function(evt) { + evt.preventDefault(); + let elem = $(this); + let container = that.edit_area; + if (elem.hasClass('active')) { + that.remove(elem.attr('name'), that.get_mode(container)); + that.serialize_to_input(); + elem.removeClass('active'); + } else { + that.add(elem.attr('name'), that.get_mode(container)); + that.serialize_to_input(); + elem.addClass('active'); + } + that.update_summary(); + }; + button.on('click', function (evt) { + evt.preventDefault(); + }).on('mousedown touchstart', + handler + ).on('mouseenter touchenter', function (evt) { + if (evt.shiftKey === false && that.pressed === false) { + return; + } + handler.bind(this)(evt); + }).on('mouseup touchend', function (evt) { + that.pressed = false; + }); + return button; + } + maxlengths() { + return { + minute: 60, + hour: 24, + dom: 31, + month: 12, + dow: 7, + year: this.end_year - this.start_year + 1 + }; + } + translate(msg) { + return i18n[this.lang][msg]; + } + monthmap() { + return i18n[this.lang].monthmap; + } + dowmap() { + return i18n[this.lang].dowmap; + } + get_edit_section(elem) { + return elem.closest('.cron-value'); + } + get_mode(elem) { + let klass = elem.attr('class'); + if (klass.indexOf('minute') !== -1) { + return 'minute'; + } else if (klass.indexOf('hour') !== -1) { + return 'hour'; + } else if (klass.indexOf('dom') !== -1) { + return 'dom'; + } else if (klass.indexOf('month') !== -1) { + return 'month'; + } else if (klass.indexOf('dow') !== -1) { + return 'dow'; + } else if (klass.indexOf('year') !== -1) { + return 'year'; + } + } + add(value, mode) { + this.value[mode].push(value.toString()); + } + remove(value, mode) { + let index = this.value[mode].indexOf(value.toString()); + if (index > -1) { + this.value[mode].splice(index, 1); + } + } + has(value, mode) { + return this.value[mode].indexOf(value.toString()) > -1; + } + parse(value) { + value = value.split(' '); + if (value.length === 5) { + value.push('*'); + } + this.parse_part(value[0].trim(), 'minute'); + this.parse_part(value[1].trim(), 'hour'); + this.parse_part(value[2].trim(), 'dom'); + this.parse_part(value[3].trim(), 'month'); + this.parse_part(value[4].trim(), 'dow'); + this.parse_part(value[5].trim(), 'year'); + } + parse_part(value, mode) { + if (typeof value === 'string') { + value = value.split(','); + } + this.value[mode] = []; + let cnt; + if (value[0] === '*') { + let start, end; + if (mode === 'minute') { + start = 0; end = 59; + } else if (mode === 'hour') { + start = 0; end = 23; + } else if (mode === 'dom') { + start = 1; end = 31; + } else if (mode === 'month') { + start = 1; end = 12; + } else if (mode === 'dow') { + start = 0; end = 6; + } else if (mode === 'year') { + start = this.start_year; + end = this.end_year; + } + for (cnt=start; cnt < end + 1; cnt++) { + this.value[mode].push(cnt.toString()); + } + } else { + let val; + for (cnt=0; cnt < value.length; cnt++) { + val = value[cnt]; + if (val === '') { + continue; + } + val = parseInt(val, 10).toString(); + this.value[mode].push(val); + } + } + } + serialize(mode) { + let vals = this.value[mode]; + vals.sort(function(a, b) { + return parseInt(a, 10) - parseInt(b, 10); + }); + let maxlength = this.maxlengths()[mode]; + if (vals.length >= maxlength) { + return '*'; + } else { + return vals.join(','); + } + } + parse_from_input(input) { + this.parse_part( + input.val(), + this.get_mode(this.get_edit_section(input)) + ); + } + serialize_to_input() { + let container = this.edit_area, + mode = this.get_mode(container), + input = $('.cron-value.' + mode + ' input', this.root); + input.val(this.serialize(mode)); + } + group_value(arr) { + let groups = [], + group = [], + idx, + nidx; + for (idx=0; idx < arr.length; idx++) { + nidx = idx + 1; + if (idx === arr.length - 1) { + group.push(arr[idx]); + groups.push(group); + } else if (parseInt(arr[idx]) + 1 === parseInt(arr[nidx])) { + group.push(arr[idx]); + } else { + group.push(arr[idx]); + groups.push(group); + group = []; + } + } + return groups; + } + update_summary() { + $('.crontab_summary .summary', this.root).html(this.summarize()); + } + summarize() { + const no_values = Object.values(this.value).every(v => Array.isArray(v) && v.length === 0); + if (no_values) { + return `
${this.translate('no_times_selected')}
` + } + return [ + this.format_part( + 'minute', 'no_minutes_selected', + 'selected_minutes','all_minutes_selected' + ), + this.format_part( + 'hour', 'no_hours_selected', + 'selected_hours', 'all_hours_selected' + ), + this.format_part( + 'dom', 'no_dom_selected', + 'selected_dom', 'all_dom_selected' + ), + this.format_part( + 'month', 'no_month_selected', + 'selected_month', 'all_month_selected', + this.monthmap() + ), + this.format_part( + 'dow', 'no_dow_selected', + 'selected_dow', 'all_dow_selected', + this.dowmap() + ), + this.format_part( + 'year', 'no_year_selected', + 'selected_years', 'all_years_selected' + ) + ].join(''); + } + format_part(value_name, no_values_selected, values_selected, + all_values_selected, value_map) { + let value = this.value[value_name], + value_len = value.length, + is_msg = false, + max_len = this.maxlengths()[value_name], + ret, + ret_values = ''; + if (value_len === 0) { + ret = this.translate(values_selected); + ret_values = this.translate(no_values_selected); + is_msg = true; + } else if (value_len < max_len) { + ret = this.translate(values_selected); + ret_values = this.format_groups( + this.group_value(value), + value_map + ); + } else { + ret = this.translate(values_selected); + ret_values = this.translate(all_values_selected); + } + return `
` + + `
${ret}
` + + `${ret_values}` + + '
'; + } + format_groups(groups, value_map) { + let ret = '', + idx, + group; + for (idx=0; idx < groups.length; idx++) { + group = groups[idx]; + if (group.length === 1) { + ret += this.display_value(group[0], value_map); + } else { + ret += this.display_value(group[0], value_map); + ret += '-'; + ret += this.display_value( + group[group.length - 1], + value_map + ); + } + if (idx !== groups.length - 1) { + ret += ', '; + } + } + return ret; + } + display_value(value, value_map) { + if (value_map) { + return value_map[value]; + } + return value; + } + }function cron_on_array_add(inst, context) { + CronWidget.initialize(context); + } + function register_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', cron_on_array_add); + } + + $(function() { + if (window.ts !== undefined) { + ts.ajax.register(CronWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(CronWidget.initialize, true); + } else { + CronWidget.initialize(); + } + register_array_subscribers(); + }); + + exports.CronWidget = CronWidget; + exports.cron_on_array_add = cron_on_array_add; + exports.i18n = i18n; + exports.register_array_subscribers = register_array_subscribers; + + Object.defineProperty(exports, '__esModule', { value: true }); + + + window.yafowil = window.yafowil || {}; + window.yafowil.cron = exports; + + + return exports; + +})({}, jQuery); diff --git a/src/yafowil/widget/cron/resources/bootstrap5/widget.min.js b/src/yafowil/widget/cron/resources/bootstrap5/widget.min.js new file mode 100644 index 0000000..fe96849 --- /dev/null +++ b/src/yafowil/widget/cron/resources/bootstrap5/widget.min.js @@ -0,0 +1 @@ +var yafowil_cron=function(e,t){"use strict";let a={en:{select_minutes:"Select Minutes",select_hours:"Select Hours",select_dom:"Select Day of Month",select_month:"Select Month",select_dow:"Select Day of Week",select_year:"Select Year",select_all:"Select All",monthmap:{1:"January",2:"February",3:"March",4:"April",5:"May",6:"June",7:"July",8:"August",9:"September",10:"October",11:"November",12:"December"},dowmap:{1:"Monday",2:"Tuesday",3:"Wednesday",4:"Thursday",5:"Friday",6:"Saturday",0:"Sunday"},summary:"Summary",no_times_selected:"No times selected",no_minutes_selected:"No minutes selected",selected_minutes:"Minutes: ",all_minutes_selected:"Every minute",no_hours_selected:"No hour selected",selected_hours:"Hours: ",all_hours_selected:"Every hour",no_dom_selected:"No day of month selected",selected_dom:"Days of month: ",all_dom_selected:"Every day of month",no_month_selected:"No month selected",selected_month:"Month: ",all_month_selected:"Every month",no_dow_selected:"No day of week selected",selected_dow:"Days of week: ",all_dow_selected:"Every day of week",no_year_selected:"No year selected",selected_years:"Years: ",all_years_selected:"Every year"},de:{select_minutes:"Minuten auswählen",select_hours:"Stunden auswählen",select_dom:"Monatstage auswählen",select_month:"Monate auswählen",select_dow:"Wochentage auswählen",select_year:"Jahre auswählen",select_all:"Alle auswählen",monthmap:{1:"Jänner",2:"Feber",3:"März",4:"April",5:"Mai",6:"Juni",7:"Juli",8:"August",9:"September",10:"Oktober",11:"November",12:"Dezember"},dowmap:{1:"Montag",2:"Dienstag",3:"Mittwoch",4:"Donnerstag",5:"Freitag",6:"Samstag",0:"Sonntag"},summary:"Zusammenfassung",no_times_selected:"Keine Zeiten ausgewählt",no_minutes_selected:"Keine Minuten ausgewählt",selected_minutes:"Minuten: ",all_minutes_selected:"Jede Minute",no_hours_selected:"Keine Stunden ausgewählt",selected_hours:"Stunden: ",all_hours_selected:"Jede Stunde",no_dom_selected:"Keine Monatstage ausgewählt",selected_dom:"Monatstage: ",all_dom_selected:"Alle Monatstage",no_month_selected:"Keine Monate ausgewählt",selected_month:"Monate: ",all_month_selected:"Jedes Monat",no_dow_selected:"Keine Wochentage ausgewählt",selected_dow:"Wochentage: ",all_dow_selected:"Jeder Wochentag",no_year_selected:"Kein Jahr ausgewählt",selected_years:"Jahre: ",all_years_selected:"Jedes Jahr"}};class s{static initialize(e){t(".crontab.widget",e).each(function(){let e=t(this);void 0!==window.yafowil_array&&window.yafowil_array.inside_template(e)||new s(e,"edit")}),t(".display-crontab.widget",e).each(function(){let e=t(this);void 0!==window.yafowil_array&&window.yafowil_array.inside_template(e)||new s(e,"display")})}constructor(e,a){e.data("yafowil-cron",this),this.root=e,this.mode=a;let s=e.data("lang");this.lang=s||"en";let n=e.data("start_year");this.start_year=n||(new Date).getFullYear();let i=e.data("end_year");this.end_year=i||(new Date).getFullYear()+9,this.pressed=!1,this.value={minute:[],hour:[],dom:[],month:[],dow:[],year:[]};let l='
'+this.translate("summary")+'

';if("display"===a)return this.parse(t("code",e).text()),e.append(t(l)),void this.update_summary();let r=this;t('input[type="hidden"]',e).each(function(){r.parse_from_input(t(this))}),e.append(t(l)),this.update_summary(),this.edit_area=t(".editarea",e),this.editarea_cls=this.edit_area.attr("class"),this.edit_area.on("mousedown touchstart",function(e){r.pressed=!0}),t(document).on("mouseup touchend",function(e){r.pressed=!1}),t("button.edit",e).on("click",function(a){a.preventDefault(),t("button.edit",e).removeClass("active"),r.show_edit_section(t(this))})}show_edit_section(e){e.addClass("active");let a,s=this.edit_area,n=this.get_edit_section(e),i=this.get_mode(n);if(s.is(":visible")&&s.hasClass(i))return n.removeClass("active"),e.removeClass("active"),e.blur(),void s.attr("class",this.editarea_cls).html("").hide();let l=t("

"),r=t('
');if("minute"===i)for(l.text(this.translate("select_minutes")),a=0;a<=59;a++)r.append(this.make_button(a,a,i));else if("hour"===i){let e;for(l.text(this.translate("select_hours")),a=1;a<=24;a++)e=a<24?a:0,r.append(this.make_button(e,e,i))}else if("dom"===i)for(l.text(this.translate("select_dom")),a=1;a<=31;a++)r.append(this.make_button(a,a,i));else if("month"===i){l.text(this.translate("select_month"));let e=this.monthmap();for(a=1;a<=12;a++)r.append(this.make_button(a,e[a],i))}else if("dow"===i){l.text(this.translate("select_dow"));let e,t=this.dowmap();for(a=1;a<=7;a++)e=a<7?a:0,r.append(this.make_button(e,t[e],i))}else if("year"===i)for(l.text(this.translate("select_year")),a=this.start_year;a<=this.end_year;a++)r.append(this.make_button(a,a,i));l=l.add(this.make_button_all(i)),r=l.add(r),s.html(r).attr("class",this.editarea_cls+" "+i).show(),n.addClass("active")}make_button_all(e){let a=t('");this.value[e].length>=this.maxlengths()[e]&&a.addClass("active");let s=this;return a.on("click",function(a){a.preventDefault();let n=t(this);n.hasClass("active")?(n.parent().find(".editcontainer button").each(function(){t(this).removeClass("active")}),n.removeClass("active"),s.parse_part("",e)):(n.parent().find(".editcontainer button").each(function(){t(this).removeClass("active").addClass("active")}),n.removeClass("active").addClass("active"),s.parse_part("*",e)),s.serialize_to_input(),s.update_summary()}),a}make_button(e,a,s){let n=t('' @@ -276,19 +239,17 @@ if (window.yafowil === undefined) { if (this.value[mode].length >= this.maxlengths()[mode]) { button.addClass('active'); } - var that = this; + let that = this; button.on('click', function(evt) { evt.preventDefault(); - var $this = $(this); + let $this = $(this); if ($this.hasClass('active')) { - // clear $this.parent().find('.editcontainer button').each(function () { $(this).removeClass('active'); }); $this.removeClass('active'); that.parse_part('', mode); } else { - // select all $this.parent().find('.editcontainer button').each(function () { $(this).removeClass('active').addClass('active'); }); @@ -299,10 +260,9 @@ if (window.yafowil === undefined) { that.update_summary(); }); return button; - }, - - make_button: function(value, name, mode) { - var button = $( + } + make_button(value, name, mode) { + let button = $( '' @@ -310,12 +270,11 @@ if (window.yafowil === undefined) { if (this.has(value, mode)) { button.addClass('active'); } - var that = this; - - var handler = function(evt) { + let that = this; + let handler = function(evt) { evt.preventDefault(); - var elem = $(this); - var container = that.edit_area; + let elem = $(this); + let container = that.edit_area; if (elem.hasClass('active')) { that.remove(elem.attr('name'), that.get_mode(container)); that.serialize_to_input(); @@ -327,7 +286,6 @@ if (window.yafowil === undefined) { } that.update_summary(); }; - button.on('click', function (evt) { evt.preventDefault(); }).on('mousedown touchstart', @@ -341,9 +299,8 @@ if (window.yafowil === undefined) { that.pressed = false; }); return button; - }, - - maxlengths: function() { + } + maxlengths() { return { minute: 60, hour: 24, @@ -352,26 +309,21 @@ if (window.yafowil === undefined) { dow: 7, year: this.end_year - this.start_year + 1 }; - }, - - translate: function(msg) { - return yafowil.cron.i18n[this.lang][msg]; - }, - - monthmap: function() { - return yafowil.cron.i18n[this.lang].monthmap; - }, - - dowmap: function() { - return yafowil.cron.i18n[this.lang].dowmap; - }, - - get_edit_section: function(elem) { + } + translate(msg) { + return i18n[this.lang][msg]; + } + monthmap() { + return i18n[this.lang].monthmap; + } + dowmap() { + return i18n[this.lang].dowmap; + } + get_edit_section(elem) { return elem.closest('.cron-value'); - }, - - get_mode: function(elem) { - var klass = elem.attr('class'); + } + get_mode(elem) { + let klass = elem.attr('class'); if (klass.indexOf('minute') !== -1) { return 'minute'; } else if (klass.indexOf('hour') !== -1) { @@ -385,27 +337,22 @@ if (window.yafowil === undefined) { } else if (klass.indexOf('year') !== -1) { return 'year'; } - }, - - add: function(value, mode) { + } + add(value, mode) { this.value[mode].push(value.toString()); - }, - - remove: function(value, mode) { - var index = this.value[mode].indexOf(value.toString()); + } + remove(value, mode) { + let index = this.value[mode].indexOf(value.toString()); if (index > -1) { this.value[mode].splice(index, 1); } - }, - - has: function(value, mode) { + } + has(value, mode) { return this.value[mode].indexOf(value.toString()) > -1; - }, - - parse: function(value) { + } + parse(value) { value = value.split(' '); if (value.length === 5) { - // year is optional value.push('*'); } this.parse_part(value[0].trim(), 'minute'); @@ -414,16 +361,15 @@ if (window.yafowil === undefined) { this.parse_part(value[3].trim(), 'month'); this.parse_part(value[4].trim(), 'dow'); this.parse_part(value[5].trim(), 'year'); - }, - - parse_part: function(value, mode) { + } + parse_part(value, mode) { if (typeof value === 'string') { value = value.split(','); } this.value[mode] = []; - var cnt; + let cnt; if (value[0] === '*') { - var start, end; + let start, end; if (mode === 'minute') { start = 0; end = 59; } else if (mode === 'hour') { @@ -442,7 +388,7 @@ if (window.yafowil === undefined) { this.value[mode].push(cnt.toString()); } } else { - var val; + let val; for (cnt=0; cnt < value.length; cnt++) { val = value[cnt]; if (val === '') { @@ -452,38 +398,33 @@ if (window.yafowil === undefined) { this.value[mode].push(val); } } - }, - - serialize: function(mode) { - var vals = this.value[mode]; + } + serialize(mode) { + let vals = this.value[mode]; vals.sort(function(a, b) { - // int-sort - otherwise it's a lexical sort. return parseInt(a, 10) - parseInt(b, 10); }); - var maxlength = this.maxlengths()[mode]; + let maxlength = this.maxlengths()[mode]; if (vals.length >= maxlength) { return '*'; } else { return vals.join(','); } - }, - - parse_from_input: function(input) { + } + parse_from_input(input) { this.parse_part( input.val(), this.get_mode(this.get_edit_section(input)) ); - }, - - serialize_to_input: function() { - var container = this.edit_area, + } + serialize_to_input() { + let container = this.edit_area, mode = this.get_mode(container), input = $('.cron-value.' + mode + ' input', this.root); input.val(this.serialize(mode)); - }, - - group_value: function(arr) { - var groups = [], + } + group_value(arr) { + let groups = [], group = [], idx, nidx; @@ -501,13 +442,11 @@ if (window.yafowil === undefined) { } } return groups; - }, - - update_summary: function() { + } + update_summary() { $('.crontab_summary .summary', this.root).html(this.summarize()); - }, - - summarize: function() { + } + summarize() { return [ this.format_part( 'minute', 'no_minutes_selected', @@ -536,12 +475,10 @@ if (window.yafowil === undefined) { 'selected_years', 'all_years_selected' ) ].join('
'); - }, - - format_part: function(value_name, no_values_selected, - values_selected, all_values_selected, - value_map) { - var value = this.value[value_name], + } + format_part(value_name, no_values_selected, values_selected, + all_values_selected, value_map) { + let value = this.value[value_name], value_len = value.length, max_len = this.maxlengths()[value_name], ret; @@ -556,10 +493,9 @@ if (window.yafowil === undefined) { ret = this.translate(all_values_selected); } return ret; - }, - - format_groups: function(groups, value_map) { - var ret = '', + } + format_groups(groups, value_map) { + let ret = '', idx, group; for (idx=0; idx < groups.length; idx++) { @@ -579,14 +515,46 @@ if (window.yafowil === undefined) { } } return ret; - }, - - display_value: function(value, value_map) { + } + display_value(value, value_map) { if (value_map) { return value_map[value]; } return value; } - }; + }function cron_on_array_add(inst, context) { + CronWidget.initialize(context); + } + function register_array_subscribers() { + if (window.yafowil_array === undefined) { + return; + } + window.yafowil_array.on_array_event('on_add', cron_on_array_add); + } + + $(function() { + if (window.ts !== undefined) { + ts.ajax.register(CronWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(CronWidget.initialize, true); + } else { + CronWidget.initialize(); + } + register_array_subscribers(); + }); + + exports.CronWidget = CronWidget; + exports.cron_on_array_add = cron_on_array_add; + exports.i18n = i18n; + exports.register_array_subscribers = register_array_subscribers; + + Object.defineProperty(exports, '__esModule', { value: true }); + + + window.yafowil = window.yafowil || {}; + window.yafowil.cron = exports; + + + return exports; -})(jQuery, yafowil); +})({}, jQuery); diff --git a/src/yafowil/widget/cron/resources/default/widget.min.css b/src/yafowil/widget/cron/resources/default/widget.min.css new file mode 100644 index 0000000..8686874 --- /dev/null +++ b/src/yafowil/widget/cron/resources/default/widget.min.css @@ -0,0 +1 @@ +.clearfix:after{clear:both;content:"";display:table}.crontab.widget .cron-value{display:inline-block;margin-right:2px}.crontab.widget .crontab_summary{margin-top:1em}.crontab.widget .editarea{display:none;width:100%}.crontab.widget .editarea.minute .editcontainer button{width:3em}.crontab.widget .editarea.minute .editcontainer button:nth-child(11),.crontab.widget .editarea.minute .editcontainer button:nth-child(21),.crontab.widget .editarea.minute .editcontainer button:nth-child(31),.crontab.widget .editarea.minute .editcontainer button:nth-child(41),.crontab.widget .editarea.minute .editcontainer button:nth-child(51){clear:left}.crontab.widget .editarea.hour .editcontainer button{width:3em}.crontab.widget .editarea.hour .editcontainer button:nth-child(13){clear:left}.crontab.widget .editarea.dow .editcontainer button{min-width:5em}.crontab.widget .editarea.dom .editcontainer button{width:3em}.crontab.widget .editarea.dom .editcontainer button:nth-child(15),.crontab.widget .editarea.dom .editcontainer button:nth-child(22),.crontab.widget .editarea.dom .editcontainer button:nth-child(29),.crontab.widget .editarea.dom .editcontainer button:nth-child(8){clear:left}.crontab.widget .editarea.month .editcontainer button{min-width:7em}.crontab.widget .editarea.month .editcontainer button:nth-child(10),.crontab.widget .editarea.month .editcontainer button:nth-child(4),.crontab.widget .editarea.month .editcontainer button:nth-child(7){clear:left}.crontab.widget .editarea.year .editcontainer button{min-width:4em}.crontab.widget .editarea.year .editcontainer button:nth-child(11),.crontab.widget .editarea.year .editcontainer button:nth-child(21),.crontab.widget .editarea.year .editcontainer button:nth-child(31),.crontab.widget .editarea.year .editcontainer button:nth-child(41),.crontab.widget .editarea.year .editcontainer button:nth-child(51),.crontab.widget .editarea.year .editcontainer button:nth-child(61),.crontab.widget .editarea.year .editcontainer button:nth-child(71),.crontab.widget .editarea.year .editcontainer button:nth-child(81),.crontab.widget .editarea.year .editcontainer button:nth-child(91){clear:left}.crontab.widget .editarea .active{background-color:var(--yafowil-accent-color,#0d6efd);border-color:var(--yafowil-accent-color,#0d6efd);color:var(--yafowil-accent-font-color,#fff)}.crontab.widget .editarea .select_all{margin:1em 0}.crontab.widget .editarea .editcontainer:after{clear:both;content:"";display:table}.crontab.widget .editarea .editcontainer button{border-radius:2px;display:block;float:left;margin:0 2px 2px 0;padding:.4em}.crontab.widget .editarea .editcontainer button:last-child:after{clear:both} \ No newline at end of file diff --git a/src/yafowil/widget/cron/resources/default/widget.min.js b/src/yafowil/widget/cron/resources/default/widget.min.js new file mode 100644 index 0000000..9a4e449 --- /dev/null +++ b/src/yafowil/widget/cron/resources/default/widget.min.js @@ -0,0 +1 @@ +var yafowil_cron=function(e,t){"use strict";let s={en:{select_minutes:"Select Minutes",select_hours:"Select Hours",select_dom:"Select Day of Month",select_month:"Select Month",select_dow:"Select Day of Week",select_year:"Select Year",select_all:"Select All",monthmap:{1:"January",2:"February",3:"March",4:"April",5:"May",6:"June",7:"July",8:"August",9:"September",10:"October",11:"November",12:"December"},dowmap:{1:"Monday",2:"Tuesday",3:"Wednesday",4:"Thursday",5:"Friday",6:"Saturday",0:"Sunday"},summary:"Summary",no_times_selected:"No times selected",no_minutes_selected:"No minutes selected",selected_minutes:"Minutes: ",all_minutes_selected:"Every minute",no_hours_selected:"No hour selected",selected_hours:"Hours: ",all_hours_selected:"Every hour",no_dom_selected:"No day of month selected",selected_dom:"Days of month: ",all_dom_selected:"Every day of month",no_month_selected:"No month selected",selected_month:"Month: ",all_month_selected:"Every month",no_dow_selected:"No day of week selected",selected_dow:"Days of week: ",all_dow_selected:"Every day of week",no_year_selected:"No year selected",selected_years:"Years: ",all_years_selected:"Every year"},de:{select_minutes:"Minuten auswählen",select_hours:"Stunden auswählen",select_dom:"Monatstage auswählen",select_month:"Monate auswählen",select_dow:"Wochentage auswählen",select_year:"Jahre auswählen",select_all:"Alle auswählen",monthmap:{1:"Jänner",2:"Feber",3:"März",4:"April",5:"Mai",6:"Juni",7:"Juli",8:"August",9:"September",10:"Oktober",11:"November",12:"Dezember"},dowmap:{1:"Montag",2:"Dienstag",3:"Mittwoch",4:"Donnerstag",5:"Freitag",6:"Samstag",0:"Sonntag"},summary:"Zusammenfassung",no_times_selected:"Keine Zeiten ausgewählt",no_minutes_selected:"Keine Minuten ausgewählt",selected_minutes:"Minuten: ",all_minutes_selected:"Jede Minute",no_hours_selected:"Keine Stunden ausgewählt",selected_hours:"Stunden: ",all_hours_selected:"Jede Stunde",no_dom_selected:"Keine Monatstage ausgewählt",selected_dom:"Monatstage: ",all_dom_selected:"Alle Monatstage",no_month_selected:"Keine Monate ausgewählt",selected_month:"Monate: ",all_month_selected:"Jedes Monat",no_dow_selected:"Keine Wochentage ausgewählt",selected_dow:"Wochentage: ",all_dow_selected:"Jeder Wochentag",no_year_selected:"Kein Jahr ausgewählt",selected_years:"Jahre: ",all_years_selected:"Jedes Jahr"}};class a{static initialize(e){t(".crontab.widget",e).each(function(){let e=t(this);void 0!==window.yafowil_array&&window.yafowil_array.inside_template(e)||new a(e,"edit")}),t(".display-crontab.widget",e).each(function(){let e=t(this);void 0!==window.yafowil_array&&window.yafowil_array.inside_template(e)||new a(e,"display")})}constructor(e,s){e.data("yafowil-cron",this),this.root=e,this.mode=s;let a=e.data("lang");this.lang=a||"en";let n=e.data("start_year");this.start_year=n||(new Date).getFullYear();let i=e.data("end_year");this.end_year=i||(new Date).getFullYear()+9,this.pressed=!1,this.value={minute:[],hour:[],dom:[],month:[],dow:[],year:[]};let o='
'+this.translate("summary")+'

';if("display"===s)return this.parse(t("code",e).text()),e.append(t(o)),void this.update_summary();let r=this;t('input[type="hidden"]',e).each(function(){r.parse_from_input(t(this))}),e.append(t(o)),this.update_summary(),this.edit_area=t(".editarea",e),this.editarea_cls=this.edit_area.attr("class"),this.edit_area.on("mousedown touchstart",function(e){r.pressed=!0}),t(document).on("mouseup touchend",function(e){r.pressed=!1}),t("button.edit",e).on("click",function(e){e.preventDefault(),r.show_edit_section(t(this))})}show_edit_section(e){let s,a=this.edit_area,n=this.get_edit_section(e),i=this.get_mode(n);if(a.is(":visible")&&a.hasClass(i))return n.removeClass("active"),void a.attr("class",this.editarea_cls).html("").hide();let o=t("

"),r=t('
');if("minute"===i)for(o.text(this.translate("select_minutes")),s=0;s<=59;s++)r.append(this.make_button(s,s,i));else if("hour"===i){let e;for(o.text(this.translate("select_hours")),s=1;s<=24;s++)e=s<24?s:0,r.append(this.make_button(e,e,i))}else if("dom"===i)for(o.text(this.translate("select_dom")),s=1;s<=31;s++)r.append(this.make_button(s,s,i));else if("month"===i){o.text(this.translate("select_month"));let e=this.monthmap();for(s=1;s<=12;s++)r.append(this.make_button(s,e[s],i))}else if("dow"===i){o.text(this.translate("select_dow"));let e,t=this.dowmap();for(s=1;s<=7;s++)e=s<7?s:0,r.append(this.make_button(e,t[e],i))}else if("year"===i)for(o.text(this.translate("select_year")),s=this.start_year;s<=this.end_year;s++)r.append(this.make_button(s,s,i));o=o.add(this.make_button_all(i)),r=o.add(r),a.html(r).attr("class",this.editarea_cls+" "+i).show(),n.addClass("active")}make_button_all(e){let s=t('");this.value[e].length>=this.maxlengths()[e]&&s.addClass("active");let a=this;return s.on("click",function(s){s.preventDefault();let n=t(this);n.hasClass("active")?(n.parent().find(".editcontainer button").each(function(){t(this).removeClass("active")}),n.removeClass("active"),a.parse_part("",e)):(n.parent().find(".editcontainer button").each(function(){t(this).removeClass("active").addClass("active")}),n.removeClass("active").addClass("active"),a.parse_part("*",e)),a.serialize_to_input(),a.update_summary()}),s}make_button(e,s,a){let n=t(' -
-
- - -
-
- - -
-
- - -
-
- - +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
-
- - -
-
""", fxml(widget())) @@ -70,7 +105,7 @@ def test_edit_renderer(self): 'start_year': 2010, 'end_year': 2020 }) - self.check_output(""" + self.checkOutput("""
* * * * *
@@ -255,7 +290,7 @@ def test_preset_values(self): 'cron', name='cronwidget', value=value) - self.check_output(""" + self.checkOutput(""" ...name="cronwidget.minute" type="hidden" value="0,10,20,30,40,50" ...name="cronwidget.hour" type="hidden" value="0,6,12,18" ...name="cronwidget.dom" type="hidden" value="1,15,30" @@ -350,7 +385,7 @@ def test_preset_values(self): 'cron', name='cronwidget', value=value) - self.check_output(""" + self.checkOutput(""" ...name="cronwidget.minute" type="hidden" value="1" ...name="cronwidget.hour" type="hidden" value="2" ...name="cronwidget.dom" type="hidden" value="3" @@ -378,7 +413,7 @@ def test_in_conjunction_with_hybrid_blueprint(self): 'leaf': True, 'div.class': 'wrapper-div' }) - self.check_output(""" + self.checkOutput("""
...
""", widget()) @@ -427,6 +462,29 @@ def test_in_conjunction_with_hybrid_blueprint(self): ['year', '2017,2018,2019', '*', []] ) + def test_resources(self): + factory.theme = 'default' + resources = factory.get_resources('yafowil.widget.cron') + self.assertTrue(resources.directory.endswith(np('/cron/resources'))) + self.assertEqual(resources.name, 'yafowil.widget.cron') + self.assertEqual(resources.path, 'yafowil-cron') + + scripts = resources.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/cron/resources/default'))) + self.assertEqual(scripts[0].path, 'yafowil-cron/default') + self.assertEqual(scripts[0].file_name, 'widget.min.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources.styles + self.assertEqual(len(styles), 1) + + self.assertTrue(styles[0].directory.endswith(np('/cron/resources/default'))) + self.assertEqual(styles[0].path, 'yafowil-cron/default') + self.assertEqual(styles[0].file_name, 'widget.min.css') + self.assertTrue(os.path.exists(styles[0].file_path)) + if __name__ == '__main__': unittest.main() diff --git a/src/yafowil/widget/cron/widget.py b/src/yafowil/widget/cron/widget.py index d043454..40878ea 100644 --- a/src/yafowil/widget/cron/widget.py +++ b/src/yafowil/widget/cron/widget.py @@ -17,13 +17,14 @@ _ = TSF('yafowil.widget.cron') +@managedprops('edit_btn_class') def cron_value_edit_action_renderer(widget, data): """Renders cron value edit button. """ return data.rendered + data.tag( 'button', attr_value('label', widget, data), - class_='btn btn-sm edit' + class_=attr_value('btn.class', widget, data) ) @@ -97,61 +98,89 @@ def cron_edit_renderer(widget, data): 'end_year': attr_value('end_year', widget, data) } }) - container['minute'] = factory( + edit_container = container['edit-container'] = factory( + 'div', + props={ + 'structural': True, + 'class': attr_value('edit_container_class', widget, data) + }) + options_container = edit_container['options'] = factory( + 'div', + props={ + 'structural': True, + 'class': attr_value('options_container_class', widget, data) + }) + options_header = options_container['options_header'] = factory( + 'div', + props={ + 'structural': True, + 'class': attr_value('options_header_class', widget, data) + }) + options_header['minute'] = factory( 'div:cron_value_edit_action:hidden', props={ 'persist': False, 'label': _('label_minute', default='Minute'), - 'div.class': 'cron-value minute' + 'div.class': 'cron-value minute', + 'btn.class': attr_value('edit_btn_class', widget, data) }) - container['hour'] = factory( + options_header['hour'] = factory( 'div:cron_value_edit_action:hidden', props={ 'persist': False, 'label': _('label_hour', default='Hour'), - 'div.class': 'cron-value hour' + 'div.class': 'cron-value hour', + 'btn.class': attr_value('edit_btn_class', widget, data) }) - container['dom'] = factory( + options_header['dom'] = factory( 'div:cron_value_edit_action:hidden', props={ 'persist': False, 'label': _('label_dom', default='Day of Month'), - 'div.class': 'cron-value dom' + 'div.class': 'cron-value dom', + 'btn.class': attr_value('edit_btn_class', widget, data) }) - container['month'] = factory( + options_header['month'] = factory( 'div:cron_value_edit_action:hidden', props={ 'persist': False, 'label': _('label_month', default='Month'), - 'div.class': 'cron-value month' + 'div.class': 'cron-value month', + 'btn.class': attr_value('edit_btn_class', widget, data) }) - container['dow'] = factory( + options_header['dow'] = factory( 'div:cron_value_edit_action:hidden', props={ 'persist': False, 'label': _('label_dow', default='Day of Week'), - 'div.class': 'cron-value dow' + 'div.class': 'cron-value dow', + 'btn.class': attr_value('edit_btn_class', widget, data) }) - container['year'] = factory( + options_header['year'] = factory( 'div:cron_value_edit_action:hidden', props={ 'persist': False, 'label': _('label_year', default='Year'), - 'div.class': 'cron-value year' + 'div.class': 'cron-value year', + 'btn.class': attr_value('edit_btn_class', widget, data) }) - container['editarea'] = factory( + edit_container['editarea'] = factory( 'div', props={ 'structural': True, - 'class': 'editarea', + 'class': attr_value('editarea_class', widget, data), }) def cron_display_renderer(widget, data): value = fetch_value(widget, data) + cssclasses = [ + attr_value("display_class", widget, data), + f'display-{attr_value("class", widget, data) or "generic"}' + ] attrs = { 'id': cssid(widget, 'display'), - 'class_': 'display-%s' % attr_value('class', widget, data) + 'class_': ' '.join([_ for _ in cssclasses if _ is not None]) } return data.tag('div', data.tag('code', value), **attrs) @@ -181,6 +210,31 @@ def cron_display_renderer(widget, data): CSS classes for cron widget wrapper DOM element. """ +factory.defaults['cron.edit_container_class'] = 'edit-container' +factory.doc['props']['cron.edit_container_class'] = """\ +CSS classes for cron widget editarea wrapper DOM element. +""" + +factory.defaults['cron.options_container_class'] = '' +factory.doc['props']['cron.options_container_class'] = """\ +CSS classes for cron widget edit options wrapper DOM element. +""" + +factory.defaults['cron.options_header_class'] = '' +factory.doc['props']['cron.options_header_class'] = """\ +CSS classes for cron widget edit options header DOM element. +""" + +factory.defaults['cron.edit_btn_class'] = 'btn btn-sm edit' +factory.doc['props']['cron.edit_btn_class'] = """\ +CSS classes for cron widget edit button DOM elements. +""" + +factory.defaults['cron.editarea_class'] = 'editarea' +factory.doc['props']['cron.editarea_class'] = """\ +CSS classes for cron widget editarea DOM element. +""" + factory.defaults['cron.lang'] = None factory.doc['props']['cron.lang'] = """\ Language code.