Projet

Général

Profil

0001-admin-revamp-overriding-forms-36711.patch

Frédéric Péters, 06 octobre 2019 18:42

Télécharger (17,6 ko)

Voir les différences:

Subject: [PATCH] admin: revamp overriding forms (#36711)

* always display summary of changes if there are data
* -> skip summary of changes if there are no data (#13599)
* do not use scrolling in summary (#32177)
* ignore no-data fields when comparing types
* warn about data loss instead of data corruption/bugs
* remove data from incompatible columns (in SQL) (#15379)
 tests/test_admin_pages.py           |  79 +++++++++++----
 wcs/admin/forms.py                  | 145 +++++++++++++++-------------
 wcs/qommon/static/css/dc2/admin.css |  12 +--
 3 files changed, 142 insertions(+), 94 deletions(-)
tests/test_admin_pages.py
1937 1937
    resp = resp.click(href='overwrite')
1938 1938
    resp.forms[0]['file'] = Upload('formdef.wcs', formdef_xml)
1939 1939
    resp = resp.forms[0].submit()
1940
    assert 'Overwrite - Summary of changes' in resp.body
1941
    resp = resp.forms[0].submit()
1940 1942
    assert FormDef.get(formdef_id).fields[0].label == '1st modified field'
1941 1943
    resp = resp.follow()
1942 1944
    assert 'The form has been successfully overwritten.' in resp.body
1943 1945

  
1944 1946
    # check with added/removed field
1945
    formdef = FormDef()
1946
    formdef.name = 'form test overwrite'
1947
    formdef.fields = [
1947
    new_formdef = FormDef()
1948
    new_formdef.name = 'form test overwrite'
1949
    new_formdef.fields = [
1948 1950
            fields.StringField(id='2', label='2nd field', type='string'),
1949 1951
            fields.StringField(id='3', label='3rd field', type='string')]
1950
    formdef_xml = ET.tostring(formdef.export_to_xml(include_id=True))
1952
    new_formdef_xml = ET.tostring(new_formdef.export_to_xml(include_id=True))
1951 1953

  
1954
    # and no data within
1955
    formdef.data_class().wipe()
1952 1956
    app = login(get_app(pub))
1953 1957
    resp = app.get('/backoffice/forms/%s/' % formdef_id)
1954 1958
    resp = resp.click(href='overwrite')
1955
    resp.forms[0]['file'] = Upload('formdef.wcs', formdef_xml)
1959
    resp.forms[0]['file'] = Upload('formdef.wcs', new_formdef_xml)
1960
    resp = resp.forms[0].submit()
1961
    assert FormDef.get(formdef_id).fields[0].id == '2'
1962
    assert FormDef.get(formdef_id).fields[0].label == '2nd field'
1963
    assert FormDef.get(formdef_id).fields[1].id == '3'
1964
    assert FormDef.get(formdef_id).fields[1].label == '3rd field'
1965

  
1966
    # and data within
1967
    formdef.store()
1968
    formdata.data = {'1': 'foo', '2': 'bar'}
1969
    formdata.just_created()
1970
    formdata.store()
1971

  
1972
    resp = app.get('/backoffice/forms/%s/' % formdef_id)
1973
    resp = resp.click(href='overwrite')
1974
    resp.forms[0]['file'] = Upload('formdef.wcs', new_formdef_xml)
1956 1975
    resp = resp.forms[0].submit()
1957
    assert 'The form removes and changes fields' in resp.body
1958
    assert not 'The form has incompatible fields, it may cause data corruption and bugs' in resp.body
1976
    assert 'The form removes or changes fields' in resp.body
1977
    assert resp.forms[0]['force'].checked is False
1978
    resp = resp.forms[0].submit()  # without checkbox (back to same form)
1979
    resp.forms[0]['force'].checked = True
1959 1980
    resp = resp.forms[0].submit()
1981

  
1960 1982
    assert FormDef.get(formdef_id).fields[0].id == '2'
1961 1983
    assert FormDef.get(formdef_id).fields[0].label == '2nd field'
1962 1984
    assert FormDef.get(formdef_id).fields[1].id == '3'
1963 1985
    assert FormDef.get(formdef_id).fields[1].label == '3rd field'
1964 1986

  
1965 1987
    # check with a field of different type
1966
    formdef = FormDef()
1967
    formdef.name = 'form test overwrite'
1968
    formdef.fields = [
1988
    formdef = FormDef.get(formdef_id)
1989
    formdata = formdef.data_class()()
1990
    formdata.data = {'1': 'foo', '2': 'bar', '3': 'baz'}
1991
    formdata.just_created()
1992
    formdata.store()
1993

  
1994
    new_formdef = FormDef()
1995
    new_formdef.name = 'form test overwrite'
1996
    new_formdef.fields = [
1969 1997
            fields.StringField(id='2', label='2nd field', type='string'),
1970
            fields.TextField(id='3', label='3rd field, text', type='text')]
1971
    formdef_xml = ET.tostring(formdef.export_to_xml(include_id=True))
1998
            fields.DateField(id='3', label='3rd field, date', type='date')]  # (string -> date)
1999
    new_formdef_xml = ET.tostring(new_formdef.export_to_xml(include_id=True))
1972 2000

  
1973 2001
    app = login(get_app(pub))
1974 2002
    resp = app.get('/backoffice/forms/%s/' % formdef_id)
1975 2003
    resp = resp.click(href='overwrite', index=0)
1976
    resp.forms[0]['file'] = Upload('formdef.wcs', formdef_xml)
2004
    resp.forms[0]['file'] = Upload('formdef.wcs', new_formdef_xml)
1977 2005
    resp = resp.forms[0].submit()
1978
    assert 'The form removes and changes fields' in resp.body
1979
    assert 'The form has incompatible fields, it may cause data corruption and bugs' in resp.body
1980
    resp.forms[0]['force'] = True
2006
    assert 'The form removes or changes fields' in resp.body
2007
    resp.forms[0]['force'].checked = True
1981 2008
    resp = resp.forms[0].submit()
1982 2009
    assert FormDef.get(formdef_id).fields[1].id == '3'
1983
    assert FormDef.get(formdef_id).fields[1].label == '3rd field, text'
1984
    assert FormDef.get(formdef_id).fields[1].type == 'text'
2010
    assert FormDef.get(formdef_id).fields[1].label == '3rd field, date'
2011
    assert FormDef.get(formdef_id).fields[1].type == 'date'
1985 2012

  
1986 2013
    # check we kept stable references
1987 2014
    assert FormDef.get(formdef_id).url_name == 'form-test'
1988 2015
    assert FormDef.get(formdef_id).table_name == 'xxx'
1989 2016

  
2017
    # check existing data
2018
    data = FormDef.get(formdef_id).data_class().get(formdata.id).data
2019
    assert data.get('2') == 'bar'
2020
    if pub.is_using_postgresql():
2021
        # in SQL, check data with different type has been removed
2022
        assert data.get('3') is None
2023

  
2024

  
1990 2025
def test_form_export_import_export_overwrite(pub):
1991 2026
    create_superuser(pub)
1992 2027
    create_role()
......
2002 2037
    ]
2003 2038
    formdef.store()
2004 2039

  
2040
    # add data
2041
    formdata = formdef.data_class()()
2042
    formdata.data = {'1': 'foo'}
2043
    formdata.just_created()
2044
    formdata.store()
2045

  
2005 2046
    formdef_xml = ET.tostring(formdef.export_to_xml(include_id=True))
2006 2047

  
2007 2048
    assert FormDef.count() == 1
......
2031 2072
    resp = resp.click(href='overwrite')
2032 2073
    resp.forms[0]['file'] = Upload('formdef.wcs', formdef2_xml)
2033 2074
    resp = resp.forms[0].submit()
2075
    assert 'Overwrite - Summary of changes' in resp.body
2076
    resp = resp.forms[0].submit()
2034 2077
    formdef_overwrited = FormDef.get(formdef.id)
2035 2078
    for i, field in enumerate(formdef2.fields):
2036 2079
        field_ow = formdef_overwrited.fields[i]
wcs/admin/forms.py
1008 1008
                form.set_error('file', msg)
1009 1009
            raise ValueError()
1010 1010

  
1011
        if form.get_widget('new_formdef').parse():
1012
            # it's been through the summary page.
1013
            if form.get_widget('force').parse():
1014
                # doing it!
1015
                return self.overwrite_by_formdef(new_formdef)
1011
        # it's been through the summary page, or there is no data yet
1012
        if not self.formdef.data_class().count() or form.get_widget('force').parse():
1013
            # doing it!
1014
            return self.overwrite_by_formdef(new_formdef)
1016 1015

  
1017
        # 1. map field id and types
1018
        current_fields = {}
1019
        new_fields = {}
1020
        for field in self.formdef.fields:
1021
            current_fields[field.id] = field.type
1022
        for field in new_formdef.fields:
1023
            new_fields[field.id] = field.type
1024

  
1025
        # 2. compare with current fields
1026
        removed_fields = []
1027
        different_type_fields = []
1028
        for field_id, field_type in current_fields.items():
1029
            if field_id not in new_fields:
1030
                removed_fields.append(field_id)
1031
            elif new_fields.get(field_id) != field_type:
1032
                different_type_fields.append(field_id)
1033

  
1034
        if removed_fields or different_type_fields:
1035
            return self.overwrite_warning_summary(new_formdef,
1036
                    removed_fields, different_type_fields)
1037

  
1038
        return self.overwrite_by_formdef(new_formdef)
1016
        return self.overwrite_warning_summary(new_formdef)
1039 1017

  
1040 1018
    def overwrite_by_formdef(self, new_formdef):
1019
        incompatible_field_ids = self.get_incompatible_field_ids(new_formdef)
1020
        if incompatible_field_ids:
1021
            # if there are incompatible field ids, remove them first
1022
            self.formdef.fields = [x for x in self.formdef.fields if x.id not in incompatible_field_ids]
1023
            self.formdef.store()
1024

  
1041 1025
        # keep current formdef id, url_name, internal identifier and sql table name
1042 1026
        new_formdef.id = self.formdef.id
1043 1027
        new_formdef.internal_identifier = self.formdef.internal_identifier
......
1056 1040
        get_session().message = ('info', _(self.overwrite_success_message))
1057 1041
        return redirect('.')
1058 1042

  
1059
    def overwrite_warning_summary(self, new_formdef, removed_fields, different_type_fields):
1043
    def get_incompatible_field_ids(self, new_formdef):
1044
        incompatible_field_ids = []
1045
        current_fields = {}
1046
        for field in self.formdef.fields:
1047
            current_fields[field.id] = field
1048

  
1049
        for field in new_formdef.fields:
1050
            current_field = current_fields.get(field.id)
1051
            if current_field and current_field.type != field.type:
1052
                incompatible_field_ids.append(field.id)
1053

  
1054
        return incompatible_field_ids
1055

  
1056
    def overwrite_warning_summary(self, new_formdef):
1060 1057
        self.html_top(title = _('Overwrite'))
1061 1058
        get_response().breadcrumb.append( ('overwrite', _('Overwrite')) )
1062 1059
        r = TemplateIO(html=True)
1063 1060

  
1064
        r += htmltext('<h2>%s</h2>') % _('Overwrite')
1065
        r += htmltext('<h3>%s</h3>') % _('Summary of changes')
1066

  
1067
        r += htmltext('<p>%s</p>') % _(
1068
                'The form removes and changes fields, you should review the '
1069
                'changes carefully.')
1061
        r += htmltext('<h2>%s - %s</h2>') % (_('Overwrite'), _('Summary of changes'))
1070 1062

  
1071 1063
        current_fields_list = [str(x.id) for x in self.formdef.fields]
1072 1064
        new_fields_list = [str(x.id) for x in new_formdef.fields]
......
1078 1070
        for field in new_formdef.fields:
1079 1071
            new_fields[field.id] = field
1080 1072

  
1081
        r += htmltext('<div id="form-diff">')
1082
        r += htmltext('<div>')
1083
        r += htmltext('<table id="table-diff">')
1073
        table = TemplateIO(html=True)
1074
        table += htmltext('<table id="table-diff">')
1075

  
1084 1076
        def ellipsize_html(field):
1085 1077
            return misc.ellipsize(field.unhtmled_label, 60)
1086 1078

  
1079
        nodata_types = ('page', 'title', 'subtitle', 'comment')
1080
        display_warning = False
1081

  
1087 1082
        for diffinfo in difflib.ndiff(current_fields_list, new_fields_list):
1088 1083
            if diffinfo[0] == '?':
1089 1084
                # detail line, ignored
1090 1085
                continue
1091 1086
            field_id = diffinfo[2:].split()[0]
1087
            current_field = current_fields.get(field_id)
1088
            new_field = new_fields.get(field_id)
1089

  
1090
            current_label = ellipsize_html(current_field) if current_field else ''
1091
            new_label = ellipsize_html(new_field) if new_field else ''
1092

  
1092 1093
            if diffinfo[0] == ' ':
1093 1094
                # unchanged line
1094
                label1 = ellipsize_html(current_fields.get(field_id))
1095
                label2 = ellipsize_html(new_fields.get(field_id))
1096
                if current_fields.get(field_id) and new_fields.get(field_id) and \
1097
                        current_fields.get(field_id).type != new_fields.get(field_id).type:
1098
                    r += htmltext('<tr class="type-change"><td class="indicator">!</td>')
1099
                if current_fields.get(field_id) and new_fields.get(field_id) and \
1100
                        ET.tostring(current_fields.get(field_id).export_to_xml('utf-8')) != \
1101
                        ET.tostring(new_fields.get(field_id).export_to_xml('utf-8')):
1102
                    r += htmltext('<tr class="modified-field"><td class="indicator">~</td>')
1095
                if current_field and new_field and current_field.type != new_field.type:
1096
                    # different datatypes
1097
                    if current_field.type in nodata_types:
1098
                        # but current field doesn't hold data, not a problem
1099
                        table += htmltext('<tr class="added-field"><td class="indicator">+</td>')
1100
                        current_label = ''
1101
                    elif new_field.type in nodata_types:
1102
                        # new field won't hold data, but old data will be removed
1103
                        table += htmltext('<tr class="removed-field"><td class="indicator">-</td>')
1104
                        new_label = ''
1105
                        display_warning = True
1106
                    else:
1107
                        # and real incompatibility, data will need to be wiped out.
1108
                        table += htmltext('<tr class="type-change"><td class="indicator">!</td>')
1109
                        display_warning = True
1110
                elif current_field and new_field and \
1111
                        ET.tostring(current_field.export_to_xml('utf-8')) != \
1112
                        ET.tostring(new_field.export_to_xml('utf-8')):
1113
                    # same type, but changes within field
1114
                    table += htmltext('<tr class="modified-field"><td class="indicator">~</td>')
1103 1115
                else:
1104
                    r += htmltext('<tr><td class="indicator"></td>')
1105
                r += htmltext('<td>%s</td> <td>%s</td></tr>') % (label1, label2)
1116
                    table += htmltext('<tr><td class="indicator"></td>')
1106 1117
            elif diffinfo[0] == '-':
1107 1118
                # removed field
1108
                label1 = ellipsize_html(current_fields.get(field_id))
1109
                if current_fields.get(field_id) and new_fields.get(field_id) and \
1110
                        current_fields.get(field_id).type != new_fields.get(field_id).type:
1111
                    r += htmltext('<tr class="type-change"><td class="indicator">!</td>')
1112
                else:
1113
                    r += htmltext('<tr class="removed-field"><td class="indicator">-</td>')
1114
                r += htmltext('<td>%s</td> <td></td></tr>') % label1
1119
                table += htmltext('<tr class="removed-field"><td class="indicator">-</td>')
1120
                display_warning = True
1115 1121
            elif diffinfo[0] == '+':
1116 1122
                # added field
1117
                label2 = ellipsize_html(new_fields.get(field_id))
1118
                if current_fields.get(field_id) and new_fields.get(field_id) and \
1119
                        current_fields.get(field_id).type != new_fields.get(field_id).type:
1120
                    r += htmltext('<tr class="type-change"><td class="indicator">!</td>')
1121
                else:
1122
                    r += htmltext('<tr class="added-field"><td class="indicator">+</td>')
1123
                r += htmltext('<td></td> <td>%s</td></tr>') % label2
1123
                table += htmltext('<tr class="added-field"><td class="indicator">+</td>')
1124
            table += htmltext('<td>%s</td> <td>%s</td></tr>') % (current_label, new_label)
1125
        table += htmltext('</table>')
1124 1126

  
1125
        r += htmltext('</table>')
1126
        r += htmltext('</div>')
1127
        if display_warning:
1128
            r += htmltext('<div class="errornotice"><p>%s</p></div>') % _(
1129
                    'The form removes or changes fields, you should review the '
1130
                    'changes carefully as some data will be lost.')
1131

  
1132
        r += htmltext('<div class="section">')
1133
        r += htmltext('<div id="form-diff">')
1134
        r += table.getvalue()
1127 1135

  
1128 1136
        r += htmltext('<div id="legend">')
1129 1137
        r += htmltext('<table>')
......
1142 1150
                _('Incompatible field'))
1143 1151
        r += htmltext('</table>')
1144 1152
        r += htmltext('</div>') # .legend
1145
        r += htmltext('</div>')
1146 1153

  
1147 1154
        get_request().method = 'GET'
1148 1155
        get_request().form = {}
1149 1156
        form = Form(enctype='multipart/form-data', use_tokens=False)
1150
        if different_type_fields:
1151
            form.widgets.append(HtmlWidget('<div class="errornotice"><p>%s</p></div>' % _(
1152
                'The form has incompatible fields, it may cause data corruption and bugs.')))
1153
            form.add(CheckboxWidget, 'force', title=_('Overwrite nevertheless'))
1157
        if display_warning:
1158
            form.add(CheckboxWidget, 'force', title=_('Overwrite despite data loss'))
1154 1159
        else:
1155 1160
            form.add_hidden('force', 'ok')
1156 1161
        form.add_hidden('new_formdef', ET.tostring(new_formdef.export_to_xml(include_id=True)))
1157 1162
        form.add_submit('submit', _('Submit'))
1158 1163
        form.add_submit('cancel', _('Cancel'))
1159 1164
        r += form.render()
1165
        r += htmltext('</div>')  # #form-diff
1166
        r += htmltext('</div>')  # .section
1160 1167

  
1161 1168
        return r.getvalue()
1162 1169

  
wcs/qommon/static/css/dc2/admin.css
1164 1164
	padding-top: 24px;
1165 1165
}
1166 1166

  
1167
div#form-diff div {
1168
	max-height: 25em;
1169
	overflow-y: scroll;
1170
}
1171

  
1172 1167
div#form-diff td {
1173 1168
	width: 50%;
1174 1169
	padding: 0.5ex;
1175 1170
}
1176 1171

  
1177 1172
div#form-diff td.indicator {
1178
	width: 5px;
1173
	width: 20px;
1174
	min-width: 20px;
1179 1175
	padding: 0 3px;
1176
	font-weight: bold;
1180 1177
}
1181 1178

  
1182 1179
div#form-diff tr td.indicator {
......
1214 1211
}
1215 1212

  
1216 1213
div#form-diff div#legend table td.indicator {
1217
	width: 5px;
1214
	width: 20px;
1215
	min-width: 20px;
1218 1216
}
1219 1217

  
1220 1218
div#form-diff div#legend td {
1221
-