├── .bumpversion.toml ├── .coveragerc ├── .editorconfig ├── .github ├── matrix.py └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── pyproject.toml ├── requirements ├── local.txt └── testing.txt ├── tests ├── __init__.py ├── settings.py └── test_blocks.py ├── tox.ini └── wagtail_link_block ├── __init__.py ├── blocks.py ├── locale └── fr │ └── LC_MESSAGES │ ├── django.mo │ └── django.po ├── static └── link_block │ ├── link_block.css │ └── link_block.js ├── templates ├── blocks │ └── link_block.html └── wagtailadmin │ └── block_forms │ └── link_block.html └── wagtail_hooks.py /.bumpversion.toml: -------------------------------------------------------------------------------- 1 | [tool.bumpversion] 2 | current_version = "1.1.7" 3 | parse = "(?P\\d+)\\.(?P\\d+)(\\.(?P\\d+))?" 4 | serialize = [ 5 | "{major}.{minor}.{patch}", 6 | "{major}.{minor}", 7 | ] 8 | search = "{current_version}" 9 | replace = "{new_version}" 10 | regex = false 11 | ignore_missing_version = false 12 | ignore_missing_files = false 13 | tag = true 14 | sign_tags = false 15 | tag_name = "{new_version}" 16 | tag_message = "Bump version: {current_version} → {new_version}" 17 | allow_dirty = false 18 | commit = true 19 | message = "Bump version: {current_version} → {new_version}" 20 | commit_args = "" 21 | 22 | [[tool.bumpversion.files]] 23 | filename = "pyproject.toml" 24 | search = "version = '{current_version}'" 25 | replace = "version = '{new_version}'" 26 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | parallel = true 4 | include = 5 | wagtail_link_block/** 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | end_of_line = lf 11 | charset = utf-8 12 | 13 | # Limit Python files to 99 characters 14 | [*.py] 15 | max_line_length = 99 16 | 17 | # Smaller indent for YAML 18 | [*.{yaml,yml}] 19 | indent_size = 2 20 | 21 | # Makefiles always use tabs for indentation 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.github/matrix.py: -------------------------------------------------------------------------------- 1 | import fileinput 2 | import json 3 | import re 4 | import sys 5 | 6 | PY_VERSIONS_RE = re.compile(r"^py(\d)(\d+)") 7 | 8 | 9 | def main(): 10 | actions_matrix = [] 11 | 12 | for tox_env in fileinput.input(): 13 | tox_env = tox_env.rstrip() 14 | 15 | if python_match := PY_VERSIONS_RE.match(tox_env): 16 | version_tuple = python_match.groups() 17 | else: 18 | version_tuple = sys.version_info[0:2] 19 | 20 | python_version = "{}.{}".format(*version_tuple) 21 | actions_matrix.append( 22 | { 23 | "python": python_version, 24 | "tox_env": tox_env, 25 | } 26 | ) 27 | 28 | print(json.dumps(actions_matrix)) # noqa:T201 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: pull_request 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 5 | cancel-in-progress: true 6 | jobs: 7 | matrix: 8 | name: Build test matrix 9 | runs-on: ubuntu-24.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | with: 14 | persist-credentials: false 15 | ref: ${{ github.event.pull_request.head.sha }} 16 | - name: Setup Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: "3.12" 20 | cache: "pip" 21 | cache-dependency-path: "requirements/*.txt" 22 | - name: Run tox 23 | id: matrix 24 | run: | 25 | pip install $(grep -E "^(tox|tox-uv)==" requirements/local.txt) 26 | echo "tox_matrix=$(tox -l | fgrep -v coverage | python .github/matrix.py)" >> $GITHUB_OUTPUT 27 | outputs: 28 | tox_matrix: ${{ steps.matrix.outputs.tox_matrix }} 29 | 30 | test: 31 | name: Test -- ${{ matrix.tox_env }} 32 | runs-on: ubuntu-24.04 33 | needs: matrix 34 | strategy: 35 | matrix: 36 | include: ${{ fromJson(needs.matrix.outputs.tox_matrix) }} 37 | fail-fast: false 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | with: 42 | persist-credentials: false 43 | ref: ${{ github.event.pull_request.head.sha }} 44 | - name: Setup Python 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: ${{ matrix.python }} 48 | cache: "pip" 49 | cache-dependency-path: "requirements/*.txt" 50 | - name: Run tests 51 | run: | 52 | pip install $(grep -E "^(tox|tox-uv)==" requirements/local.txt) 53 | tox -e ${{ matrix.tox_env }} 54 | - name: Upload coverage data 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: coverage-data-${{ matrix.tox_env }} 58 | include-hidden-files: true 59 | path: .coverage.* 60 | if-no-files-found: ignore 61 | 62 | coverage: 63 | name: Coverage 64 | runs-on: ubuntu-24.04 65 | needs: test 66 | if: always() 67 | steps: 68 | - name: Checkout 69 | uses: actions/checkout@v4 70 | with: 71 | persist-credentials: false 72 | ref: ${{ github.event.pull_request.head.sha }} 73 | - name: Setup Python 74 | uses: actions/setup-python@v5 75 | with: 76 | python-version: "3.12" 77 | cache: "pip" 78 | cache-dependency-path: "requirements/*.txt" 79 | - uses: actions/download-artifact@v4 80 | with: 81 | pattern: coverage-data-* 82 | merge-multiple: true 83 | - name: Run coverage 84 | run: | 85 | pip install $(grep -E "^(tox|tox-uv)==" requirements/local.txt) 86 | tox -e coverage 87 | tox -qq exec -e coverage -- coverage report --format=markdown >> $GITHUB_STEP_SUMMARY 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | MANIFEST 3 | dist 4 | build 5 | *.egg-info 6 | .*_cache 7 | 8 | # Tox 9 | .tox/ 10 | .coverage 11 | .coverage.* 12 | htmlcov/ 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2024, Developer Society Limited 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune tests 2 | graft wagtail_link_block 3 | global-exclude *.py[co] 4 | global-exclude __pycache__ 5 | global-exclude .DS_Store 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | .DEFAULT_GOAL := help 3 | 4 | 5 | # --------------------------------- 6 | # Project specific targets 7 | # --------------------------------- 8 | # 9 | # Add any targets specific to the current project in here. 10 | 11 | 12 | 13 | # ------------------------------- 14 | # Common targets for DEV projects 15 | # ------------------------------- 16 | # 17 | # Edit these targets so they work as expected on the current project. 18 | # 19 | # Remember there may be other tools which use these targets, so if a target is not suitable for 20 | # the current project, then keep the target and simply make it do nothing. 21 | 22 | help: ## This help dialog. 23 | help: help-display 24 | 25 | clean: ## Remove unneeded files generated from the various build tasks. 26 | clean: build-clean 27 | 28 | reset: ## Reset your local environment. Useful after switching branches, etc. 29 | reset: venv-check venv-wipe install-local 30 | 31 | check: ## Check for any obvious errors in the project's setup. 32 | check: pipdeptree-check 33 | 34 | format: ## Run this project's code formatters. 35 | format: ruff-format 36 | 37 | lint: ## Lint the project. 38 | lint: ruff-lint 39 | 40 | test: ## Run unit and integration tests. 41 | test: django-test 42 | 43 | test-report: ## Run and report on unit and integration tests. 44 | test-report: coverage-clean test coverage-report 45 | 46 | test-lowest: ## Run tox with lowest (oldest) package dependencies. 47 | test-lowest: tox-test-lowest 48 | 49 | package: ## Builds source and wheel packages 50 | package: clean build-package 51 | 52 | 53 | # --------------- 54 | # Utility targets 55 | # --------------- 56 | # 57 | # Targets which are used by the common targets. You likely want to customise these per project, 58 | # to ensure they're pointing at the correct directories, etc. 59 | 60 | # Build 61 | build-clean: 62 | rm -rf build 63 | rm -rf dist 64 | rm -rf .eggs 65 | find . -maxdepth 1 -name '*.egg-info' -exec rm -rf {} + 66 | 67 | build-package: 68 | python -m build 69 | twine check --strict dist/* 70 | check-wheel-contents dist/*.whl 71 | 72 | 73 | # Virtual Environments 74 | venv-check: 75 | ifndef VIRTUAL_ENV 76 | $(error Must be in a virtualenv) 77 | endif 78 | 79 | venv-wipe: venv-check 80 | if ! pip list --format=freeze | grep -v "^pip=\|^setuptools=\|^wheel=" | xargs pip uninstall -y; then \ 81 | echo "Nothing to remove"; \ 82 | fi 83 | 84 | 85 | # Installs 86 | install-local: pip-install-local 87 | 88 | 89 | # Pip 90 | pip-install-local: venv-check 91 | pip install -r requirements/local.txt 92 | 93 | 94 | # Coverage 95 | coverage-report: coverage-combine coverage-html 96 | coverage report --show-missing 97 | 98 | coverage-combine: 99 | coverage combine 100 | 101 | coverage-html: 102 | coverage html 103 | 104 | coverage-clean: 105 | rm -rf htmlcov 106 | rm -f .coverage 107 | 108 | 109 | # ruff 110 | ruff-lint: 111 | ruff check 112 | ruff format --check 113 | 114 | ruff-format: 115 | ruff check --fix-only 116 | ruff format 117 | 118 | 119 | # pipdeptree 120 | pipdeptree-check: 121 | pipdeptree --warn fail >/dev/null 122 | 123 | 124 | # Project testing 125 | django-test: 126 | PYTHONWARNINGS=all coverage run $$(which django-admin) test --pythonpath $$(pwd) --settings tests.settings tests 127 | 128 | tox-test-lowest: 129 | tox --recreate --override testenv.uv_resolution=lowest 130 | 131 | 132 | # Help 133 | help-display: 134 | @awk '/^[\-[:alnum:]]*: ##/ { split($$0, x, "##"); printf "%20s%s\n", x[1], x[2]; }' $(MAKEFILE_LIST) 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wagtail Link Block 2 | 3 | A link block to use as part of other StructBlocks which lets the user choose a link either to a 4 | Page, Document, external URL, Email, telephone or anchor within the current page and whether or 5 | not they want the link to open in a new window. 6 | 7 | It hides the unused fields, making the admin clearer and less cluttered. 8 | 9 | ## Installation 10 | 11 | Using [pip](https://pip.pypa.io/): 12 | 13 | ```console 14 | $ pip install wagtail-link-block 15 | ``` 16 | 17 | Edit your Django project's settings module, and add the application to ``INSTALLED_APPS``: 18 | 19 | ```python 20 | INSTALLED_APPS = [ 21 | # ... 22 | "wagtail_link_block", 23 | # ... 24 | ] 25 | ``` 26 | 27 | ## Usage 28 | 29 | To use in a block 30 | 31 | ```python 32 | from wagtail_link_block.blocks import LinkBlock 33 | 34 | class MyButton(StructBlock): 35 | text = CharBlock() 36 | link = LinkBlock() 37 | 38 | class Meta: 39 | template = "blocks/my_button_block.html" 40 | ``` 41 | 42 | And create the template `blocks/my_button_block.html`: 43 | 44 | ```html 45 | 46 | {{ self.text }} 47 | 48 | ``` 49 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = 'wagtail-link-block' 3 | version = '1.1.7' 4 | description = 'A Block for Wagtail that lets users choose a Page/Document/URL/email/etc. as a link' 5 | readme = 'README.md' 6 | maintainers = [{ name = 'The Developer Society', email = 'studio@dev.ngo' }] 7 | requires-python = '>= 3.9' 8 | dependencies = [ 9 | 'Wagtail>=5.2', 10 | ] 11 | classifiers = [ 12 | 'Intended Audience :: Developers', 13 | 'License :: OSI Approved :: BSD License', 14 | 'Operating System :: OS Independent', 15 | 'Programming Language :: Python', 16 | 'Programming Language :: Python :: 3', 17 | 'Programming Language :: Python :: 3.9', 18 | 'Programming Language :: Python :: 3.10', 19 | 'Programming Language :: Python :: 3.11', 20 | 'Programming Language :: Python :: 3.12', 21 | 'Programming Language :: Python :: 3.13', 22 | 'Framework :: Django', 23 | 'Framework :: Django :: 4.2', 24 | 'Framework :: Django :: 5.1', 25 | 'Framework :: Django :: 5.2', 26 | 'Framework :: Wagtail', 27 | 'Framework :: Wagtail :: 5', 28 | 'Framework :: Wagtail :: 6', 29 | 'Framework :: Wagtail :: 7', 30 | ] 31 | 32 | [project.urls] 33 | Homepage = "https://github.com/developersociety/wagtail-link-block" 34 | 35 | [build-system] 36 | requires = ['setuptools >= 61.0'] 37 | build-backend = 'setuptools.build_meta' 38 | 39 | [tool.setuptools.packages.find] 40 | include = ['wagtail_link_block*'] 41 | 42 | [tool.ruff] 43 | line-length = 99 44 | target-version = 'py39' 45 | 46 | [tool.ruff.lint] 47 | select = [ 48 | 'F', # pyflakes 49 | 'E', # pycodestyle 50 | 'W', # pycodestyle 51 | 'I', # isort 52 | 'N', # pep8-naming 53 | 'UP', # pyupgrade 54 | 'S', # flake8-bandit 55 | 'BLE', # flake8-blind-except 56 | 'C4', # flake8-comprehensions 57 | 'EM', # flake8-errmsg 58 | 'T20', # flake8-print 59 | 'RET', # flake8-return 60 | 'RUF', # ruff 61 | ] 62 | ignore = [ 63 | 'EM101', # flake8-errmsg: raw-string-in-exception 64 | ] 65 | 66 | [tool.ruff.lint.isort] 67 | combine-as-imports = true 68 | -------------------------------------------------------------------------------- /requirements/local.txt: -------------------------------------------------------------------------------- 1 | -r testing.txt 2 | 3 | bump-my-version==0.24.3 4 | tox==4.16.0 5 | tox-uv==1.10.0 6 | -------------------------------------------------------------------------------- /requirements/testing.txt: -------------------------------------------------------------------------------- 1 | build==1.2.1 2 | check-wheel-contents==0.6.0 3 | coverage==7.6.0 4 | pipdeptree==2.23.1 5 | ruff==0.5.5 6 | twine==5.1.1 7 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developersociety/wagtail-link-block/ac4f1b5d3fe96f9ec565de877e10bc85efc19f13/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 2 | 3 | USE_TZ = True 4 | 5 | SECRET_KEY = "secret" # noqa:S105 6 | 7 | INSTALLED_APPS = [ 8 | "django.contrib.admin", 9 | "django.contrib.auth", 10 | "django.contrib.contenttypes", 11 | "django.contrib.messages", 12 | "django.contrib.sessions", 13 | "django.contrib.staticfiles", 14 | "modelcluster", 15 | "taggit", 16 | "wagtail.admin", 17 | "wagtail.contrib.forms", 18 | "wagtail.contrib.redirects", 19 | "wagtail.documents", 20 | "wagtail.embeds", 21 | "wagtail.images", 22 | "wagtail.search", 23 | "wagtail.sites", 24 | "wagtail.snippets", 25 | "wagtail.users", 26 | "wagtail", 27 | "wagtail_link_block", 28 | ] 29 | 30 | STORAGES = { 31 | "default": { 32 | "BACKEND": "django.core.files.storage.FileSystemStorage", 33 | }, 34 | "staticfiles": { 35 | "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage", 36 | }, 37 | } 38 | 39 | MIDDLEWARE = [ 40 | "django.middleware.security.SecurityMiddleware", 41 | "django.contrib.sessions.middleware.SessionMiddleware", 42 | "django.middleware.common.CommonMiddleware", 43 | "django.middleware.csrf.CsrfViewMiddleware", 44 | "django.contrib.auth.middleware.AuthenticationMiddleware", 45 | "django.contrib.messages.middleware.MessageMiddleware", 46 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 47 | "wagtail.contrib.redirects.middleware.RedirectMiddleware", 48 | ] 49 | 50 | STATIC_URL = "/static/" 51 | 52 | TEMPLATES = [ 53 | { 54 | "BACKEND": "django.template.backends.django.DjangoTemplates", 55 | "APP_DIRS": True, 56 | "OPTIONS": { 57 | "context_processors": [ 58 | "django.template.context_processors.debug", 59 | "django.template.context_processors.request", 60 | "django.contrib.auth.context_processors.auth", 61 | "django.contrib.messages.context_processors.messages", 62 | ], 63 | }, 64 | }, 65 | ] 66 | 67 | WAGTAILADMIN_BASE_URL = "http://testserver" 68 | -------------------------------------------------------------------------------- /tests/test_blocks.py: -------------------------------------------------------------------------------- 1 | from django.test import SimpleTestCase 2 | 3 | from wagtail_link_block.blocks import LinkBlock 4 | 5 | 6 | class LinkBlockTestCase(SimpleTestCase): 7 | def test_block(self): 8 | block = LinkBlock() 9 | 10 | self.assertIsNotNone(block.render({})) 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | env_list = 3 | check 4 | lint 5 | py{39,310,311,312}-django4.2-wagtail{5.2,6.3,6.4,7.0} 6 | py{310,311,312,313}-django{5.1,5.2}-wagtail{6.3,6.4,7.0} 7 | coverage 8 | no_package = true 9 | 10 | [testenv] 11 | deps = 12 | -rrequirements/testing.txt 13 | django4.2: Django>=4.2,<5.0 14 | django5.1: Django>=5.1,<5.2 15 | django5.2: Django>=5.2,<5.3 16 | wagtail5.2: wagtail>=5.2,<5.3 17 | wagtail6.3: wagtail>=6.3,<6.4 18 | wagtail6.4: wagtail>=6.4,<6.5 19 | wagtail7.0: wagtail>=7.0,<7.1 20 | allowlist_externals = make 21 | commands = make test 22 | package = editable 23 | 24 | [testenv:check] 25 | base_python = python3.12 26 | commands = make check 27 | uv_seed = true 28 | 29 | [testenv:lint] 30 | base_python = python3.12 31 | commands = make lint 32 | 33 | [testenv:coverage] 34 | base_python = python3.12 35 | commands = make coverage-report 36 | -------------------------------------------------------------------------------- /wagtail_link_block/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developersociety/wagtail-link-block/ac4f1b5d3fe96f9ec565de877e10bc85efc19f13/wagtail_link_block/__init__.py -------------------------------------------------------------------------------- /wagtail_link_block/blocks.py: -------------------------------------------------------------------------------- 1 | """ 2 | The LinkBlock is not designed to be used on it's own - but as part of other blocks. 3 | """ 4 | 5 | from copy import deepcopy 6 | 7 | from django.forms.utils import ErrorList 8 | from django.utils.translation import gettext_lazy as _ 9 | from wagtail.admin.forms.choosers import URLOrAbsolutePathValidator 10 | from wagtail.blocks import ( 11 | BooleanBlock, 12 | CharBlock, 13 | ChoiceBlock, 14 | EmailBlock, 15 | PageChooserBlock, 16 | StreamBlockValidationError, 17 | StructBlock, 18 | StructValue, 19 | ) 20 | from wagtail.documents.blocks import DocumentChooserBlock 21 | 22 | ############################################################################## 23 | # Component Parts - should not be used on their own - but as parts of other 24 | # blocks: 25 | 26 | 27 | class URLValue(StructValue): 28 | """ 29 | Get active link used in LinkBlock or CustomLinkBlock if there is one 30 | """ 31 | 32 | def get_url(self): 33 | link_to = self.get("link_to") 34 | 35 | if link_to in ("page", "file"): 36 | # If file or page check obj is not None 37 | if self.get(link_to): 38 | return self.get(link_to).url 39 | elif link_to == "custom_url": 40 | return self.get(link_to) 41 | elif link_to == "anchor": 42 | return "#" + self.get(link_to) 43 | elif link_to == "email": 44 | return f"mailto:{self.get(link_to)}" 45 | elif link_to == "phone": 46 | return f"tel:{self.get(link_to)}" 47 | return None 48 | 49 | def get_link_to(self): 50 | """ 51 | Return link type for accessing in templates 52 | """ 53 | return self.get("link_to") 54 | 55 | 56 | class LinkBlock(StructBlock): 57 | """ 58 | A Link which can either be to a (off-site) URL, to a page in the site, 59 | or to a document. Use this instead of URLBlock. 60 | """ 61 | 62 | link_to = ChoiceBlock( 63 | choices=[ 64 | ("page", _("Page")), 65 | ("file", _("File")), 66 | ("custom_url", _("Custom URL")), 67 | ("email", _("Email")), 68 | ("anchor", _("Anchor")), 69 | ("phone", _("Phone")), 70 | ], 71 | required=False, 72 | classname="link_choice_type_selector", 73 | label=_("Link to"), 74 | ) 75 | page = PageChooserBlock(required=False, classname="page_link", label=_("Page")) 76 | file = DocumentChooserBlock(required=False, classname="file_link", label=_("File")) 77 | custom_url = CharBlock( 78 | max_length=300, 79 | required=False, 80 | classname="custom_url_link url_field", 81 | validators=[URLOrAbsolutePathValidator()], 82 | label=_("Custom URL"), 83 | ) 84 | anchor = CharBlock( 85 | max_length=300, 86 | required=False, 87 | classname="anchor_link", 88 | label=_("#"), 89 | ) 90 | email = EmailBlock(required=False) 91 | phone = CharBlock(max_length=30, required=False, classname="phone_link", label=_("Phone")) 92 | 93 | new_window = BooleanBlock( 94 | label=_("Open in new window"), required=False, classname="new_window_toggle" 95 | ) 96 | 97 | class Meta: 98 | label = None 99 | value_class = URLValue 100 | icon = "fa-share-square" 101 | form_classname = "link_block" 102 | form_template = "wagtailadmin/block_forms/link_block.html" 103 | template = "blocks/link_block.html" 104 | 105 | def __init__(self, *args, **kwargs) -> None: 106 | super().__init__(*args, **kwargs) 107 | # Make a deep copy of the link-to, as we need to pass the 108 | # 'required' option down to it, and don't want to pollute 109 | # the other LinkBlocks that are defined on other parent blocks. 110 | self.child_blocks["link_to"] = deepcopy(self.child_blocks["link_to"]) 111 | self.child_blocks["link_to"].field.required = kwargs.get("required", False) 112 | 113 | def set_name(self, name): 114 | """ 115 | Over ride StructBlock set_name so label can remain empty in streamblocks 116 | """ 117 | self.name = name 118 | 119 | def clean(self, value): 120 | clean_values = super().clean(value) 121 | errors = {} 122 | 123 | url_default_values = { 124 | "page": None, 125 | "file": None, 126 | "custom_url": "", 127 | "anchor": "", 128 | "email": "", 129 | "phone": "", 130 | } 131 | url_type = clean_values.get("link_to") 132 | 133 | # Check that a value has been uploaded for the chosen link type 134 | if url_type != "" and clean_values.get(url_type) in [None, ""]: 135 | errors[url_type] = ErrorList( 136 | ["You need to add a {} link".format(url_type.replace("_", " "))] 137 | ) 138 | else: 139 | try: 140 | # Remove values added for link types not selected 141 | url_default_values.pop(url_type, None) 142 | for field in url_default_values: 143 | clean_values[field] = url_default_values[field] 144 | except KeyError: 145 | errors[url_type] = ErrorList(["Enter a valid link type"]) 146 | 147 | if errors: 148 | raise StreamBlockValidationError(block_errors=errors, non_block_errors=ErrorList([])) 149 | 150 | return clean_values 151 | -------------------------------------------------------------------------------- /wagtail_link_block/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/developersociety/wagtail-link-block/ac4f1b5d3fe96f9ec565de877e10bc85efc19f13/wagtail_link_block/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /wagtail_link_block/locale/fr/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: PACKAGE VERSION\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2021-09-24 18:42+0200\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: wagtail_link_block/blocks.py:56 wagtail_link_block/blocks.py:61 22 | msgid "Page" 23 | msgstr "Page" 24 | 25 | #: wagtail_link_block/blocks.py:56 wagtail_link_block/blocks.py:62 26 | msgid "File" 27 | msgstr "Fichier" 28 | 29 | #: wagtail_link_block/blocks.py:56 wagtail_link_block/blocks.py:68 30 | msgid "Custom URL" 31 | msgstr "URL" 32 | 33 | #: wagtail_link_block/blocks.py:56 34 | msgid "Anchor" 35 | msgstr "Ancre" 36 | 37 | #: wagtail_link_block/blocks.py:59 38 | msgid "Link to" 39 | msgstr "Type de lien" 40 | 41 | #: wagtail_link_block/blocks.py:74 42 | msgid "#" 43 | msgstr "#" 44 | 45 | #: wagtail_link_block/blocks.py:77 46 | msgid "Open in new window" 47 | msgstr "Ouvrir dans une nouvelle fenêtre" 48 | -------------------------------------------------------------------------------- /wagtail_link_block/static/link_block/link_block.css: -------------------------------------------------------------------------------- 1 | .link-block__hidden { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /wagtail_link_block/static/link_block/link_block.js: -------------------------------------------------------------------------------- 1 | /* global window */ 2 | (function() { 3 | 'use strict'; 4 | 5 | // For Links, only show the selected field types. So if 'Page Link' is selected, 6 | // only show the Page Chooser. If URL field, only show the URL field. Etc. 7 | 8 | function setRelatedFieldsVisibility(link_type_selector) { 9 | const value = link_type_selector.value; 10 | const parent = link_type_selector.closest('.link_block'), 11 | page_link = parent.querySelector('.page_link_field'), 12 | file_link = parent.querySelector('.file_link_field'), 13 | custom_url_link = parent.querySelector('.custom_url_link_field'), 14 | anchor_link = parent.querySelector('.anchor_link_field'), 15 | new_window_toggle = parent.querySelector('.new_window_link_field'), 16 | email_address = parent.querySelector('.email_link_field'), 17 | phone_link = parent.querySelector('.phone_link_field'); 18 | 19 | // first hide all 20 | page_link.classList.add('link-block__hidden'); 21 | file_link.classList.add('link-block__hidden'); 22 | custom_url_link.classList.add('link-block__hidden'); 23 | anchor_link.classList.add('link-block__hidden'); 24 | new_window_toggle.classList.add('link-block__hidden'); 25 | email_address.classList.add('link-block__hidden'); 26 | phone_link.classList.add('link-block__hidden'); 27 | 28 | // display the one 29 | if (value === 'page') { 30 | page_link.classList.remove('link-block__hidden'); 31 | new_window_toggle.classList.remove('link-block__hidden'); 32 | } else if (value === 'file') { 33 | file_link.classList.remove('link-block__hidden'); 34 | } else if (value === 'custom_url') { 35 | custom_url_link.classList.remove('link-block__hidden'); 36 | new_window_toggle.classList.remove('link-block__hidden'); 37 | } else if (value === 'anchor') { 38 | anchor_link.classList.remove('link-block__hidden'); 39 | new_window_toggle.classList.remove('link-block__hidden'); 40 | } else if (value === 'email') { 41 | email_address.classList.remove('link-block__hidden'); 42 | } else if (value === 'phone') { 43 | phone_link.classList.remove('link-block__hidden'); 44 | } else { 45 | // I don't know what to display here. 46 | } 47 | } 48 | 49 | function onload() { 50 | const active_selectors = document.querySelectorAll('.link_choice_type_selector select'); 51 | 52 | // Show link options if a link has been chosen 53 | // prototype call to make IE happy. 54 | Array.prototype.forEach.call(active_selectors, setRelatedFieldsVisibility); 55 | } 56 | 57 | function onchange(event) { 58 | const target = event.target, 59 | link_choice_div = target.closest('.link_choice_type_selector'); 60 | 61 | if (link_choice_div !== null) { 62 | setRelatedFieldsVisibility(target); 63 | } 64 | } 65 | 66 | window.addEventListener('load', onload); 67 | window.addEventListener('change', onchange); 68 | 69 | })(); 70 | -------------------------------------------------------------------------------- /wagtail_link_block/templates/blocks/link_block.html: -------------------------------------------------------------------------------- 1 | {{ self.text }} 2 | -------------------------------------------------------------------------------- /wagtail_link_block/templates/wagtailadmin/block_forms/link_block.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 | {# extended to add `link_block_field` and default `hidden` classes to link detail fields. #} 3 |
4 | {% if help_text %} 5 | 6 |
7 | 8 | {{ help_text }} 9 |
10 |
11 | {% endif %} 12 | 13 | {% for child in children.values %} 14 | 29 | {% endfor %} 30 |
31 | -------------------------------------------------------------------------------- /wagtail_link_block/wagtail_hooks.py: -------------------------------------------------------------------------------- 1 | from django.templatetags.static import static 2 | from django.utils.html import format_html 3 | from wagtail import hooks 4 | 5 | 6 | @hooks.register("insert_global_admin_css") 7 | def global_admin_css(): 8 | return format_html('', static("link_block/link_block.css")) 9 | 10 | 11 | @hooks.register("insert_global_admin_js") 12 | def global_admin_js(): 13 | return format_html('', static("link_block/link_block.js")) 14 | --------------------------------------------------------------------------------