From 52150749a666188af6cc8a3423d306ea6b8eb67f Mon Sep 17 00:00:00 2001 From: adrian-turjak Date: Wed, 19 Jul 2017 13:25:27 +1200 Subject: [PATCH] Fixes to allow us to use Django 1.11 Mostly the issue with Horizon was we were using some undocumented widgets that were cut for version 1.11 django. This patch adds their code into our repo for now, but we need to do a proper rewrite of our widgets for Queens to use current widgets in Django. Also edits some tests because the functionality changed in Django 1.11 Implements: blueprint dj111 Co-Authored-By: Rob Cresswell Change-Id: I444d45f274662f4f33701c16cce4ae80cb546654 --- horizon/forms/fields.py | 221 +++++++++++++++++- horizon/test/helpers.py | 2 +- .../dashboards/project/instances/tests.py | 21 +- 3 files changed, 234 insertions(+), 10 deletions(-) diff --git a/horizon/forms/fields.py b/horizon/forms/fields.py index a9235b097f..6b33c1ccba 100644 --- a/horizon/forms/fields.py +++ b/horizon/forms/fields.py @@ -28,8 +28,10 @@ from django.forms.utils import flatatt from django.forms import widgets from django.template.loader import get_template from django.utils.encoding import force_text +from django.utils.encoding import python_2_unicode_compatible from django.utils.functional import Promise from django.utils import html +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$') @@ -156,8 +158,14 @@ class MACAddressField(fields.Field): return str(getattr(self, "mac_address", "")) -class SelectWidget(widgets.Select): - """Customizable select widget. +# NOTE(adriant): The Select widget was considerably rewritten in Django 1.11 +# and broke our customizations because we relied on the inner workings of +# this widget as it was written. I've opted to move that older variant of the +# select widget here as a custom widget for Horizon, but this should be +# reviewed and replaced in future. We need to move to template based rendering +# for widgets, but that's a big task better done in Queens. +class SelectWidget(widgets.Widget): + """Custom select widget. It allows to render data-xxx attributes from choices. This widget also allows user to specify additional html attributes @@ -210,10 +218,29 @@ class SelectWidget(widgets.Select): """ def __init__(self, attrs=None, choices=(), data_attrs=(), transform=None, transform_html_attrs=None): + self.choices = list(choices) self.data_attrs = data_attrs self.transform = transform self.transform_html_attrs = transform_html_attrs - super(SelectWidget, self).__init__(attrs, choices) + super(SelectWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + if value is None: + value = '' + final_attrs = self.build_attrs(attrs, name=name) + output = [html.format_html('', flatatt(final_attrs))] + options = self.render_options([value]) + if options: + output.append(options) + output.append('') + return mark_safe('\n'.join(output)) + + def build_attrs(self, extra_attrs=None, **kwargs): + "Helper function for building an attribute dictionary." + attrs = dict(self.attrs, **kwargs) + if extra_attrs: + attrs.update(extra_attrs) + return attrs def render_option(self, selected_choices, option_value, option_label): option_value = force_text(option_value) @@ -231,6 +258,23 @@ class SelectWidget(widgets.Select): return u'' % ( html.escape(option_value), other_html, option_label) + def render_options(self, selected_choices): + # Normalize to strings. + selected_choices = set(force_text(v) for v in selected_choices) + output = [] + for option_value, option_label in self.choices: + if isinstance(option_label, (list, tuple)): + output.append(html.format_html( + '', force_text(option_value))) + for option in option_label: + output.append( + self.render_option(selected_choices, *option)) + output.append('') + else: + output.append(self.render_option( + selected_choices, option_value, option_label)) + return '\n'.join(output) + def get_data_attrs(self, option_label): other_html = [] if not isinstance(option_label, (six.string_types, Promise)): @@ -390,7 +434,104 @@ class ThemableCheckboxInput(widgets.CheckboxInput): ) -class ThemableCheckboxChoiceInput(widgets.CheckboxChoiceInput): +# NOTE(adriant): SubWidget was removed in Django 1.11 and thus has been moved +# to our codebase until we redo how we handle widgets. +@html.html_safe +@python_2_unicode_compatible +class SubWidget(object): + """SubWidget class from django 1.10.7 codebase + + Some widgets are made of multiple HTML elements -- namely, RadioSelect. + This is a class that represents the "inner" HTML element of a widget. + """ + def __init__(self, parent_widget, name, value, attrs, choices): + self.parent_widget = parent_widget + self.name, self.value = name, value + self.attrs, self.choices = attrs, choices + + def __str__(self): + args = [self.name, self.value, self.attrs] + if self.choices: + args.append(self.choices) + return self.parent_widget.render(*args) + + +# NOTE(adriant): ChoiceInput and CheckboxChoiceInput were removed in +# Django 1.11 so ChoiceInput has been moved to our codebase until we redo how +# we handle widgets. +@html.html_safe +@python_2_unicode_compatible +class ChoiceInput(SubWidget): + """ChoiceInput class from django 1.10.7 codebase + + An object used by ChoiceFieldRenderer that represents a single + . + """ + input_type = None # Subclasses must define this + + def __init__(self, name, value, attrs, choice, index): + self.name = name + self.value = value + self.attrs = attrs + self.choice_value = force_text(choice[0]) + self.choice_label = force_text(choice[1]) + self.index = index + if 'id' in self.attrs: + self.attrs['id'] += "_%d" % self.index + + def __str__(self): + return self.render() + + def render(self, name=None, value=None, attrs=None): + if self.id_for_label: + label_for = html.format_html(' for="{}"', self.id_for_label) + else: + label_for = '' + # NOTE(adriant): OrderedDict used to make html attrs order + # consistent for testing. + attrs = dict(self.attrs, **attrs) if attrs else self.attrs + return html.format_html( + '{} {}', + label_for, + self.tag(attrs), + self.choice_label + ) + + def is_checked(self): + return self.value == self.choice_value + + def tag(self, attrs=None): + attrs = attrs or self.attrs + # NOTE(adriant): OrderedDict used to make html attrs order + # consistent for testing. + final_attrs = dict( + attrs, + type=self.input_type, + name=self.name, + value=self.choice_value) + if self.is_checked(): + final_attrs['checked'] = 'checked' + return html.format_html('', flatatt(final_attrs)) + + @property + def id_for_label(self): + return self.attrs.get('id', '') + + +# NOTE(adriant): CheckboxChoiceInput was removed in Django 1.11 so this widget +# has been expanded to include the functionality inherieted previously as a +# temporary solution until we redo how we handle widgets. +class ThemableCheckboxChoiceInput(ChoiceInput): + + input_type = 'checkbox' + + def __init__(self, *args, **kwargs): + super(ThemableCheckboxChoiceInput, self).__init__(*args, **kwargs) + self.value = set(force_text(v) for v in self.value) + + def is_checked(self): + return self.choice_value in self.value + def render(self, name=None, value=None, attrs=None, choices=()): if self.id_for_label: label_for = html.format_html(' for="{}"', self.id_for_label) @@ -404,7 +545,77 @@ class ThemableCheckboxChoiceInput(widgets.CheckboxChoiceInput): ) -class ThemableCheckboxFieldRenderer(widgets.CheckboxFieldRenderer): +# NOTE(adriant): CheckboxFieldRenderer was removed in Django 1.11 so +# has been moved here until we redo how we handle widgets. +@html.html_safe +@python_2_unicode_compatible +class CheckboxFieldRenderer(object): + """CheckboxFieldRenderer class from django 1.10.7 codebase + + An object used by RadioSelect to enable customization of radio widgets. + """ + + choice_input_class = None + outer_html = '{content}' + inner_html = '
  • {choice_value}{sub_widgets}
  • ' + + def __init__(self, name, value, attrs, choices): + self.name = name + self.value = value + self.attrs = attrs + self.choices = choices + + def __getitem__(self, idx): + return list(self)[idx] + + def __iter__(self): + for idx, choice in enumerate(self.choices): + yield self.choice_input_class( + self.name, self.value, self.attrs.copy(), choice, idx) + + def __str__(self): + return self.render() + + def render(self): + """Outputs a
      for this set of choice fields. + + If an id was given to the field, it is applied to the
        (each + item in the list will get an id of `$id_$i`). + """ + id_ = self.attrs.get('id') + output = [] + for i, choice in enumerate(self.choices): + choice_value, choice_label = choice + if isinstance(choice_label, (tuple, list)): + attrs_plus = self.attrs.copy() + if id_: + attrs_plus['id'] += '_{}'.format(i) + sub_ul_renderer = self.__class__( + name=self.name, + value=self.value, + attrs=attrs_plus, + choices=choice_label, + ) + sub_ul_renderer.choice_input_class = self.choice_input_class + output.append(html.format_html( + self.inner_html, choice_value=choice_value, + sub_widgets=sub_ul_renderer.render(), + )) + else: + w = self.choice_input_class( + self.name, self.value, self.attrs.copy(), choice, i) + output.append(html.format_html( + self.inner_html, + choice_value=force_text(w), + sub_widgets='')) + return html.format_html( + self.outer_html, + id_attr=html.format_html(' id="{}"', id_) if id_ else '', + content=mark_safe('\n'.join(output)), + ) + + +class ThemableCheckboxFieldRenderer(CheckboxFieldRenderer): choice_input_class = ThemableCheckboxChoiceInput diff --git a/horizon/test/helpers.py b/horizon/test/helpers.py index 98ac493e8f..6fa0b6bf47 100644 --- a/horizon/test/helpers.py +++ b/horizon/test/helpers.py @@ -145,7 +145,7 @@ class TestCase(django_test.TestCase): def _setup_request(self): self.request = http.HttpRequest() - self.request.session = self.client._session() + self.request.session = self.client.session def tearDown(self): super(TestCase, self).tearDown() diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py index b62ca24432..28021f1406 100644 --- a/openstack_dashboard/dashboards/project/instances/tests.py +++ b/openstack_dashboard/dashboards/project/instances/tests.py @@ -1737,11 +1737,24 @@ class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase): else: self.assertNotContains(res, boot_from_image_field_label) - checked_box = '= (1, 11): + checked_box = ( + '' + ) else: - self.assertNotContains(res, checked_box) + checked_box = ( + '' + ) + if only_one_network: + self.assertContains(res, checked_box, html=True) + else: + self.assertNotContains(res, checked_box, html=True) disk_config_field_label = 'Disk Partition' if disk_config: