0001-admin-revamp-overriding-forms-36711.patch
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 |
- |