diff --git a/README.md b/README.md index 3265d3bc..cc621db4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # CDH Django libraries -A collection of Django apps for CDH/DH-IT Django projects. Developed by the +A collection of Django apps for CDH Django projects. Developed by the [ILS Labs](https://github.com/UiL-OTS-labs) and the -[DH-IT Faculty Portal Development and Support Team](https://github.com/DH-IT-Portal-Development/django-shared-core) +[Humanities IT Portal Development and Support Team](https://github.com/orgs/CentreForDigitalHumanities/teams/portal-development) -Documentation provided here: https://dh-it-portal-development.github.io/django-shared-core/ +Documentation provided here: https://centrefordigitalhumanities.github.io/django-shared-core/ ## Currently targeting: - Python 3.9 @@ -21,7 +21,7 @@ specifics. Add the following line to your python requirements: -``cdh-django-core[all] @ git+https://github.com/DH-IT-Portal-Development/django-shared-core.git@`` +``cdh-django-core[all] @ git+https://github.com/CentreForDigitalHumanities/django-shared-core.git@`` Replacing ```` with the latest DSC release tag. (e.g. ``v3.1.0``). @@ -36,7 +36,7 @@ The library can be installed with a reduced dependency set for the apps your project uses. To do this, replace the ``all`` with a comma-separated list of the apps your project uses (sans ``cdh.``). For example: -``cdh-django-core[core,files,rest] @ git+https://github.com/DH-IT-Portal-Development/django-shared-core.git@[version]`` +``cdh-django-core[core,files,rest] @ git+https://github.com/CentreForDigitalHumanities/django-shared-core.git@[version]`` ## App collection diff --git a/dev/main/forms.py b/dev/main/forms.py index b33c823a..77a47daf 100644 --- a/dev/main/forms.py +++ b/dev/main/forms.py @@ -1,6 +1,7 @@ from django import forms from django.urls import reverse_lazy from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from cdh.core import fields as core_fields from cdh.core.forms import SplitMonthInput, TemplatedForm, TemplatedModelForm @@ -13,17 +14,11 @@ class FormStylesForm(forms.Form): text = forms.CharField() - textarea = forms.CharField( - widget=forms.Textarea - ) + textarea = forms.CharField(widget=forms.Textarea) - django_date = forms.DateField( - help_text="Using django's version of the field" - ) + django_date = forms.DateField(help_text="Using django's version of the field") - django_time = forms.TimeField( - help_text="Using django's version of the field" - ) + django_time = forms.TimeField(help_text="Using django's version of the field") django_datetime = forms.DateTimeField( help_text="Using django's version of the field" @@ -33,13 +28,9 @@ class FormStylesForm(forms.Form): help_text="Using django's version of the field" ) - core_date = core_fields.DateField( - help_text="Using core's version of the field" - ) + core_date = core_fields.DateField(help_text="Using core's version of the field") - core_time = core_fields.TimeField( - help_text="Using core's version of the field" - ) + core_time = core_fields.TimeField(help_text="Using core's version of the field") core_datetime = core_fields.DateTimeField( help_text="Using core's version of the field" @@ -51,27 +42,31 @@ class FormStylesForm(forms.Form): checkbox = forms.BooleanField() - choice = forms.ChoiceField(choices=[ - (1, "Train"), - (2, "Bus"), - (3, "Aeroplane"), - (4, "Bike"), - (5, "Feet"), - (6, "Magical Unicorn"), - (6, "Broom"), - (6, "Thestrals"), - ]) - - typed_choice = forms.TypedChoiceField(choices=[ - (1, "Train"), - (2, "Bus"), - (3, "Aeroplane"), - (4, "Bike"), - (5, "Feet"), - (6, "Magical Unicorn"), - (6, "Broom"), - (6, "Thestrals"), - ]) + choice = forms.ChoiceField( + choices=[ + (1, "Train"), + (2, "Bus"), + (3, "Aeroplane"), + (4, "Bike"), + (5, "Feet"), + (6, "Magical Unicorn"), + (6, "Broom"), + (6, "Thestrals"), + ] + ) + + typed_choice = forms.TypedChoiceField( + choices=[ + (1, "Train"), + (2, "Bus"), + (3, "Aeroplane"), + (4, "Bike"), + (5, "Feet"), + (6, "Magical Unicorn"), + (6, "Broom"), + (6, "Thestrals"), + ] + ) radio = forms.TypedChoiceField( choices=[ @@ -84,7 +79,7 @@ class FormStylesForm(forms.Form): (6, "Broom"), (6, "Thestrals"), ], - widget=forms.RadioSelect + widget=forms.RadioSelect, ) integer = forms.IntegerField() @@ -117,6 +112,9 @@ class FormStylesForm(forms.Form): class CustomTemplateFormStylesForm(TemplatedForm): + info_header = core_fields.TemplatedFormTextField( + header=_("People involved"), classes="" + ) text = forms.CharField( label="Onderzoeksprojectnaam", @@ -127,25 +125,31 @@ class CustomTemplateFormStylesForm(TemplatedForm): label="Eindverantwoordelijke", ) + date_header = core_fields.TemplatedFormTextField( + header=_("Project duration"), header_element="h4" + ) + date_start = core_fields.DateField( label="Begin Datum", help_text="Op deze datum wordt de verwerking actief in het register " - "van verwerkingen" + "van verwerkingen", ) date_end = core_fields.DateField( label="Eind Datum", - help_text=mark_safe("Dit is de datum waarop de resultaten worden " - "gepubliceerd." - "
" - "Op deze datum gaat de archiveringstermijn van de " - "onderzoeksdata in") + help_text=mark_safe( + "Dit is de datum waarop de resultaten worden " + "gepubliceerd." + "
" + "Op deze datum gaat de archiveringstermijn van de " + "onderzoeksdata in" + ), ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for visible in self.visible_fields(): - visible.field.widget.attrs['class'] = 'form-control' + visible.field.widget.attrs["class"] = "form-control" class JqueryUIFormStylesForm(forms.Form): @@ -153,7 +157,6 @@ class JqueryUIFormStylesForm(forms.Form): class CustomEmailForm(TemplatedForm): - sender = forms.CharField() banner = forms.CharField() @@ -162,10 +165,10 @@ class CustomEmailForm(TemplatedForm): label="Mail content", help_text=ExampleCustomTemplateEmail.help_text(), widget=EmailContentEditWidget( - reverse_lazy('main:custom_email_form_preview'), - sender_field='sender', - banner_field='banner', - footer_field='footer', + reverse_lazy("main:custom_email_form_preview"), + sender_field="sender", + banner_field="banner", + footer_field="footer", ), ) @@ -174,9 +177,8 @@ class CustomEmailForm(TemplatedForm): class MonthFieldTestForm(TemplatedModelForm): show_valid_fields = False + class Meta: model = MonthFieldTest - fields = ['single_month_field', 'split_month_field'] - widgets = { - 'split_month_field': SplitMonthInput - } + fields = ["single_month_field", "split_month_field"] + widgets = {"split_month_field": SplitMonthInput} diff --git a/dev/main/templates/main/custom_styles_form.html b/dev/main/templates/main/custom_styles_form.html index 0065fe65..052caf48 100644 --- a/dev/main/templates/main/custom_styles_form.html +++ b/dev/main/templates/main/custom_styles_form.html @@ -10,7 +10,7 @@

Custom form template

Note: this required Django 4+

-
+ {% csrf_token %} {{ form }}
diff --git a/src/cdh/core/forms.py b/src/cdh/core/forms.py index b69e04cd..0f5b7edd 100644 --- a/src/cdh/core/forms.py +++ b/src/cdh/core/forms.py @@ -1,10 +1,20 @@ from datetime import date -from typing import List, Optional +from typing import List, Optional, Union from django import forms from django.core.exceptions import ValidationError -from django.forms.widgets import CheckboxInput, CheckboxSelectMultiple, \ - DateInput, MultiWidget, NumberInput, RadioSelect, Select, TextInput +from django.forms.renderers import get_default_renderer +from django.forms.widgets import ( + CheckboxInput, + CheckboxSelectMultiple, + MultiWidget, + NumberInput, + RadioSelect, + Select, + TextInput, +) +from django.utils.html import format_html +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from cdh.core.file_loading import add_js_file @@ -21,7 +31,7 @@ class TemplatedFormMixin: texts """ - template_name = 'cdh.core/form_template.html' + template_name = "cdh.core/form_template.html" # If False, it will hide the help column on every field show_help_column = True # If False, it will hide the help column on fields without a help text @@ -34,24 +44,24 @@ def get_context(self): form_was_changed = len(self.changed_data) != 0 - for field, errors in context['fields']: + for field, errors in context["fields"]: # Fix for fields that do not set this attr - if 'class' not in field.field.widget.attrs: - field.field.widget.attrs['class'] = '' + if "class" not in field.field.widget.attrs: + field.field.widget.attrs["class"] = "" - field.field.widget.attrs['class'] += ' form-control' + field.field.widget.attrs["class"] += " form-control" if errors: - field.field.widget.attrs['valid'] = 'is-invalid' - field.field.widget.attrs['class'] += ' is-invalid' + field.field.widget.attrs["valid"] = "is-invalid" + field.field.widget.attrs["class"] += " is-invalid" elif form_was_changed and self.show_valid_fields: # Only add if the data on the form was changed, as that would # indicate a validation step gone wrong. - field.field.widget.attrs['class'] += ' is-valid' - field.field.widget.attrs['valid'] = 'is-valid' + field.field.widget.attrs["class"] += " is-valid" + field.field.widget.attrs["valid"] = "is-valid" - context['show_help_column'] = self.show_help_column - context['always_show_help_column'] = self.always_show_help_column + context["show_help_column"] = self.show_help_column + context["always_show_help_column"] = self.always_show_help_column return context @@ -61,6 +71,7 @@ class TemplatedForm(TemplatedFormMixin, forms.Form): Uses :class:`.TemplatedFormMixin` """ + pass @@ -69,6 +80,7 @@ class TemplatedModelForm(TemplatedFormMixin, forms.ModelForm): Uses :class:`.TemplatedFormMixin` """ + pass @@ -76,105 +88,226 @@ class BootstrapSelect(Select): """Override of Django's version to use the right Bootstrap classes""" def get_context(self, name, value, attrs): - if 'class' not in attrs: - attrs['class'] = "" + if "class" not in attrs: + attrs["class"] = "" - if 'form-control' in attrs['class']: - attrs['class'] = attrs['class'].replace( - 'form-control', - 'form-select' - ) + if "form-control" in attrs["class"]: + attrs["class"] = attrs["class"].replace("form-control", "form-select") else: - attrs['class'] += ' form-select' + attrs["class"] += " form-select" return super().get_context(name, value, attrs) class BootstrapCheckboxInput(CheckboxInput): """Override of Django's version to use the right Bootstrap classes""" + template_name = "cdh.core/forms/widgets/bootstrap_checkbox.html" class BootstrapRadioSelect(RadioSelect): """Override of Django's version to use the right Bootstrap classes""" + template_name = "cdh.core/forms/widgets/bootstrap_radio.html" option_template_name = "cdh.core/forms/widgets/bootstrap_radio_option.html" class BootstrapCheckboxSelectMultiple(CheckboxSelectMultiple): """Override of Django's version to use the right Bootstrap classes""" + template_name = "cdh.core/forms/widgets/bootstrap_radio.html" option_template_name = "cdh.core/forms/widgets/bootstrap_radio_option.html" +class TemplatedFormTextWidget(forms.Widget): + template_name = "cdh.core/forms/widgets/text.html" + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + # Needed in the form template, to avoid rendering this inside a UU-Form + # row. + context["template_text"] = True + + return context + + +class TemplatedFormTextField(forms.Field): + widget = TemplatedFormTextWidget + form_text = True + + def __init__( + self, + *args, + caption: Optional[str] = None, + header: Optional[str] = None, + header_element: str = "h3", + classes: str = "pt-4", + template: Optional[str] = None, + template_context: Optional[Union[dict, callable]] = None, + **kwargs, + ): + """A fake field that just displays a header/some other text. + + Either use the default header with optional caption, or provide your + own template to render. + + Note: use Form.field_order to place this field where you want when + using model forms. + + Note 2: as always, make sure any used variables are safe as + they will be marked safe automatically. + Please escape any user-supplied content before it's passed to this + field. + + :param header: The header text. Ignored if template is specified + :param caption: (optional) a caption text. Ignored if template is specified + :param classes: Any CSS classes to add to the containing element. + Defaults to 'pt-4' + :param template: A template to render + :param template_context: Either a dict, or a callable returning a + dict, with template context. + """ + super().__init__(*args, **kwargs) + self.caption = caption + self.header = header + self.header_element = header_element + self.classes = classes + self.template = template + self.template_context = template_context + + def prepare_value(self, value): + """We use the value to generate the content; it's a hack, but due to + limitations of Django forms it's the cleanest way to pass it to the + widget.""" + if self.template: + renderer = get_default_renderer() + context = self.template_context + + if callable(context): + context = context() + + if context is None: + context = {} + + return mark_safe(renderer.render(self.template, context)) + + header = "" + if self.header: + header = mark_safe( + f"<{self.header_element}>{self.header}" + ) + + caption = "" + if self.caption: + caption = mark_safe(f"

{self.caption}

") + + return format_html( + '
{header}{caption}
', + classes=self.classes, + header=header, + caption=caption, + ) + + def to_python(self, value): + return None + + def validate(self, value): + pass + + def run_validators(self, value): + pass + + class PasswordField(forms.CharField): """Override of Django's version to use the right HTML5 input""" + widget = forms.PasswordInput class ColorInput(forms.TextInput): """Override of Django's version to use the right HTML5 input""" - input_type = 'color' + + input_type = "color" class ColorField(forms.CharField): """Override of Django's version to use the right HTML5 input""" + widget = ColorInput class TelephoneInput(forms.TextInput): """Override of Django's version to use the right HTML5 input""" - input_type = 'tel' + + input_type = "tel" class TelephoneField(forms.CharField): """Override of Django's version to use the right HTML5 input""" + widget = TelephoneInput class DateInput(forms.DateInput): """Override of Django's version to use the right HTML5 input""" - input_type = 'date' + + input_type = "date" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.format = self.format or "%Y-%m-%d" class DateField(forms.DateField): """Override of Django's version to use the right HTML5 input""" + widget = DateInput class TimeInput(forms.TimeInput): """Override of Django's version to use the right HTML5 input""" - input_type = 'time' + + input_type = "time" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.format = self.format or "%H:%M" class TimeField(forms.TimeField): """Override of Django's version to use the right HTML5 input""" + widget = TimeInput class DateTimeInput(forms.DateTimeInput): """Override of Django's version to use the right HTML5 input""" - input_type = 'datetime-local' + + input_type = "datetime-local" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.format = self.format or "%Y-%m-%d %H:%M" class DateTimeField(forms.DateTimeField): """Override of Django's version to use the right HTML5 input""" + widget = DateTimeInput class BootstrapMultiWidgetMixin: - template_name = 'cdh.core/forms/widgets/bootstrap_multiwidget.html' + template_name = "cdh.core/forms/widgets/bootstrap_multiwidget.html" def get_context(self, name, value, attrs): subwidgets = {} - if 'subwidgets' in self.attrs: - subwidgets = self.attrs['subwidgets'] + if "subwidgets" in self.attrs: + subwidgets = self.attrs["subwidgets"] context = super().get_context(name, value, attrs) for subwidget_name, subwidget_attrs in subwidgets.items(): - if subwidget_name in context['widget']['subwidgets']: - context['widget']['subwidgets']['attrs'].update( - subwidget_attrs) + if subwidget_name in context["widget"]["subwidgets"]: + context["widget"]["subwidgets"]["attrs"].update(subwidget_attrs) return context @@ -185,7 +318,15 @@ class BootstrapMultiWidget(BootstrapMultiWidgetMixin, MultiWidget): class SplitDateTimeWidget(forms.SplitDateTimeWidget): """Override of Django SplitDateTimeWidget to use HTML5 fields""" - def __init__(self, attrs=None, date_format=None, time_format=None, date_attrs=None, time_attrs=None): + + def __init__( + self, + attrs=None, + date_format=None, + time_format=None, + date_attrs=None, + time_attrs=None, + ): widgets = ( DateInput( attrs=attrs if date_attrs is None else date_attrs, @@ -199,31 +340,32 @@ def __init__(self, attrs=None, date_format=None, time_format=None, date_attrs=No forms.MultiWidget.__init__(self, widgets) -class BootstrapSplitDateTimeWidget(BootstrapMultiWidgetMixin, - SplitDateTimeWidget): +class BootstrapSplitDateTimeWidget(BootstrapMultiWidgetMixin, SplitDateTimeWidget): pass class BootstrapSplitDateTimeField(forms.SplitDateTimeField): """Override of Django SplitDateTimeField to use bootstrap multiwidget""" + widget = BootstrapSplitDateTimeWidget class SplitDateTimeField(forms.SplitDateTimeField): """Override of Django SplitDateTimeField to use HTML5 fields""" + widget = SplitDateTimeWidget class MonthInput(TextInput): - input_type = 'month' + input_type = "month" def __init__(self, attrs=None): if attrs is None: attrs = {} - attrs['pattern'] = "[0-9]{4}-[0-9]{1,2}" - if 'placeholder' not in attrs: - attrs['placeholder'] = 'yyyy-mm' + attrs["pattern"] = "[0-9]{4}-[0-9]{1,2}" + if "placeholder" not in attrs: + attrs["placeholder"] = "yyyy-mm" super().__init__(attrs) @@ -238,37 +380,36 @@ def format_value(self, value): class SplitMonthInput(BootstrapMultiWidget): - def __init__(self, attrs=None): month_attrs = attrs if attrs else {} year_attrs = attrs if attrs else {} - year_attrs['placeholder'] = _('core:fields:month:year_placeholder') - if hasattr(year_attrs, 'year_min'): - year_attrs['min'] = year_attrs.get('year_min') - if hasattr(year_attrs, 'year_max'): - year_attrs['max'] = year_attrs.get('year_max') + year_attrs["placeholder"] = _("core:fields:month:year_placeholder") + if hasattr(year_attrs, "year_min"): + year_attrs["min"] = year_attrs.get("year_min") + if hasattr(year_attrs, "year_max"): + year_attrs["max"] = year_attrs.get("year_max") widgets = { - 'month': BootstrapSelect( + "month": BootstrapSelect( attrs=month_attrs, choices=( - ('', _('core:fields:month:month_placeholder')), - (1, _('core:fields:month:january')), - (2, _('core:fields:month:february')), - (3, _('core:fields:month:march')), - (4, _('core:fields:month:april')), - (5, _('core:fields:month:may')), - (6, _('core:fields:month:june')), - (7, _('core:fields:month:july')), - (8, _('core:fields:month:august')), - (9, _('core:fields:month:september')), - (10, _('core:fields:month:october')), - (11, _('core:fields:month:november')), - (12, _('core:fields:month:december')), + ("", _("core:fields:month:month_placeholder")), + (1, _("core:fields:month:january")), + (2, _("core:fields:month:february")), + (3, _("core:fields:month:march")), + (4, _("core:fields:month:april")), + (5, _("core:fields:month:may")), + (6, _("core:fields:month:june")), + (7, _("core:fields:month:july")), + (8, _("core:fields:month:august")), + (9, _("core:fields:month:september")), + (10, _("core:fields:month:october")), + (11, _("core:fields:month:november")), + (12, _("core:fields:month:december")), ), ), - 'year': NumberInput( + "year": NumberInput( attrs=year_attrs, ), } @@ -294,14 +435,14 @@ def __init__(self, year_min=1970, year_max=9999, **kwargs): if isinstance(self.widget, MonthInput): # This could be done in widget_attrs, but the other widget cannot # be done there, so I prefer to keep them together - self.widget.attrs['min'] = f"{year_min}-01" - self.widget.attrs['max'] = f"{year_max}-12" + self.widget.attrs["min"] = f"{year_min}-01" + self.widget.attrs["max"] = f"{year_max}-12" if isinstance(self.widget, SplitMonthInput): # We have to manually set these attrs to the right widget, # as MultiWidget has not other way to set these month_widget, year_widget = self.widget.widgets - year_widget.attrs['min'] = year_min - year_widget.attrs['max'] = year_max + year_widget.attrs["min"] = year_min + year_widget.attrs["max"] = year_max def to_python(self, value): if value is None or value == "": @@ -310,7 +451,7 @@ def to_python(self, value): # MonthInput returns a string if isinstance(value, str): try: - year, month = value.split('-', maxsplit=1) + year, month = value.split("-", maxsplit=1) year = int(year) month = int(month) @@ -322,9 +463,7 @@ def to_python(self, value): # Catches both the int conversions, and the cases where splitting # doesn't work except (ValueError, TypeError): - raise ValidationError( - self.error_messages["invalid"], code="invalid" - ) + raise ValidationError(self.error_messages["invalid"], code="invalid") else: # SplitMonthInput returns a list of [month, year] month, year = value @@ -336,9 +475,7 @@ def to_python(self, value): year = int(year) month = int(month) except (ValueError, TypeError): - raise ValidationError( - self.error_messages["invalid"], code="invalid" - ) + raise ValidationError(self.error_messages["invalid"], code="invalid") try: if month and year: @@ -347,15 +484,14 @@ def to_python(self, value): return Month(year, month) except ValueError: - raise ValidationError( - self.error_messages["invalid"], code="invalid" - ) + raise ValidationError(self.error_messages["invalid"], code="invalid") raise ValidationError(self.error_messages["invalid"], code="invalid") class TinyMCEWidget(forms.Widget): """A TinyMCE widget for HTML editting""" + template_name = "cdh.core/forms/widgets/tinymce.html" class Media: js = [ @@ -365,14 +501,14 @@ class Media: ] def __init__( - self, - menubar: bool = False, - plugins: Optional[List[str]] = None, - toolbar: Optional[str] = 'undo redo casechange blocks bold ' - 'italic underline link bullist numlist' - ' | code', - *args, - **kwargs + self, + menubar: bool = False, + plugins: Optional[List[str]] = None, + toolbar: Optional[str] = "undo redo casechange blocks bold " + "italic underline link bullist numlist" + " | code", + *args, + **kwargs, ): """ All parameters should have sensible defaults. @@ -386,7 +522,12 @@ def __init__( if plugins is None: plugins = [ - 'link', 'image', 'visualblocks', 'wordcount', 'lists', 'code', + "link", + "image", + "visualblocks", + "wordcount", + "lists", + "code", ] self.menubar = menubar @@ -396,9 +537,8 @@ def __init__( def get_context(self, *args, **kwargs): context = super().get_context(*args, **kwargs) - context['menubar'] = self.menubar - context['plugins'] = ",".join(self.plugins) - context['toolbar'] = self.toolbar + context["menubar"] = self.menubar + context["plugins"] = ",".join(self.plugins) + context["toolbar"] = self.toolbar return context - diff --git a/src/cdh/core/templates/cdh.core/form_template.html b/src/cdh/core/templates/cdh.core/form_template.html index 56278ad6..2ae3d535 100644 --- a/src/cdh/core/templates/cdh.core/form_template.html +++ b/src/cdh/core/templates/cdh.core/form_template.html @@ -6,33 +6,37 @@ {% endif %} {% for field, errors in fields %} -
-
- - {{ field }} -
- {% for error in field.errors %} - {{ error }} - {% if not forloop.last %} -
- {% endif %} - {% endfor %} + {% if field.field.form_text %} + {{ field }} + {% else %} +
+
+ + {{ field }} +
+ {% for error in field.errors %} + {{ error }} + {% if not forloop.last %} +
+ {% endif %} + {% endfor %} +
+ {% if show_help_column and field.help_text %} +
+ {% if field.help_text %} + {{ field.help_text|safe }} + {% endif %} +
+ {% endif %}
- {% if show_help_column and field.help_text %} -
- {% if field.help_text %} - {{ field.help_text|safe }} - {% endif %} -
- {% endif %} -
+ {% endif %} {% endfor %} {% for field in hidden_fields %}{{ field }}{% endfor %} diff --git a/src/cdh/core/templates/cdh.core/forms/widgets/text.html b/src/cdh/core/templates/cdh.core/forms/widgets/text.html new file mode 100644 index 00000000..12ad3517 --- /dev/null +++ b/src/cdh/core/templates/cdh.core/forms/widgets/text.html @@ -0,0 +1,3 @@ +
+ {{ widget.value }} +
\ No newline at end of file