Skip to content

Commit 1ecb82a

Browse files
committed
feat(dev): new ckan code;
- Updated code for new ckan ds fk error handling.
1 parent 49d162e commit 1ecb82a

File tree

2 files changed

+91
-135
lines changed

2 files changed

+91
-135
lines changed

ckanext/recombinant/utils.py

Lines changed: 0 additions & 76 deletions
This file was deleted.

ckanext/recombinant/views.py

Lines changed: 91 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
from flask_babel import force_locale
33
import re
44
import simplejson as json
5-
from markupsafe import Markup
65
from sqlalchemy import text as sql_text
76

8-
from typing import Union, Dict, Tuple, Any
7+
from typing import Union, Dict, Tuple, Any, Optional, List
98
from ckan.types import Response
109

1110
from werkzeug.datastructures import FileStorage as FlaskFileStorage
@@ -46,7 +45,6 @@
4645
from ckanext.recombinant.tables import get_chromo, get_geno
4746
from ckanext.recombinant.helpers import (
4847
recombinant_primary_key_fields, recombinant_choice_fields)
49-
from ckanext.recombinant.utils import get_constraint_error_from_psql_error
5048

5149
from io import BytesIO
5250

@@ -55,7 +53,8 @@
5553
from ckanext.datastore.backend import DatastoreBackend
5654
from ckanext.datastore.backend.postgres import (
5755
DatastorePostgresqlBackend,
58-
identifier as sql_identifier
56+
identifier as sql_identifier,
57+
_parse_constraint_error_from_psql_error
5958
)
6059

6160
from ckanapi import NotFound, LocalCKAN
@@ -102,13 +101,12 @@ def upload(id: str) -> Response:
102101
owner_org=org['name'])
103102
except BadExcelData as e:
104103
h.flash_error(_(e.message))
105-
session['RECOMBINANT_ERRORS'] = _(e.message)
106104
return h.redirect_to('recombinant.preview_table',
107105
resource_name=resource_name,
108106
owner_org=org['name'])
109107

110108

111-
@recombinant.route('/recombinant/delete/<id>/<resource_id>', methods=['POST'])
109+
@recombinant.route('/recombinant/delete/<id>/<resource_id>', methods=['GET', 'POST'])
112110
def delete_records(id: str, resource_id: str) -> Union[str, Response]:
113111
lc = LocalCKAN(username=g.user)
114112
filters = {}
@@ -126,17 +124,25 @@ def delete_records(id: str, resource_id: str) -> Union[str, Response]:
126124
dataset = lc.action.recombinant_show(
127125
dataset_type=pkg['type'], owner_org=org['name'])
128126

129-
def delete_error(err: Union[str, Markup]) -> Union[str, Markup]:
130-
session['RECOMBINANT_DELETE_ERRORS'] = err
131-
session['RECOMBINANT_FILTERS'] = filters
132-
return h.redirect_to(
133-
'recombinant.preview_table',
134-
resource_name=res['name'],
135-
owner_org=org['name'])
127+
def delete_error(err: str, _records: Optional[List[str]]) -> str:
128+
return render('recombinant/confirm_delete.html',
129+
extra_vars={'dataset': dataset,
130+
'resource': res,
131+
'errors': [err],
132+
'num': len(_records),
133+
'bulk_delete': '\n'.join(
134+
_records
135+
# extra blank is needed to prevent field
136+
# from being completely empty
137+
+ ([''] if '' in _records else []))})
136138

137139
form_text = request.form.get('bulk-delete', '')
138140
if not form_text:
139-
return delete_error(_('Required field'))
141+
# we can just silently refresh
142+
return h.redirect_to(
143+
'recombinant.preview_table',
144+
resource_name=res['name'],
145+
owner_org=org['name'])
140146

141147
pk_fields = recombinant_primary_key_fields(res['name'])
142148

@@ -146,11 +152,11 @@ def delete_error(err: Union[str, Markup]) -> Union[str, Markup]:
146152
for r in records:
147153
r = r.rstrip('\r')
148154

149-
def record_fail(err: Union[str, Markup]) -> Union[str, Markup]:
155+
def record_fail(err: str) -> str:
150156
# move bad record to the top of the pile
151157
filters['bulk-delete'] = '\n'.join(
152158
[r] + list(records) + ok_records)
153-
return delete_error(err)
159+
return delete_error(err, ok_records)
154160

155161
split_on = '\t' if '\t' in r else ','
156162
fields = [f for f in r.split(split_on)]
@@ -183,8 +189,8 @@ def record_fail(err: Union[str, Markup]) -> Union[str, Markup]:
183189
return h.redirect_to(
184190
'recombinant.preview_table',
185191
resource_name=res['name'],
186-
owner_org=org['name'],)
187-
if 'confirm' not in request.form:
192+
owner_org=org['name'])
193+
if 'confirm' not in request.form or request.method == 'GET':
188194
return render('recombinant/confirm_delete.html',
189195
extra_vars={'dataset': dataset,
190196
'resource': res,
@@ -194,31 +200,23 @@ def record_fail(err: Union[str, Markup]) -> Union[str, Markup]:
194200
# extra blank is needed to prevent field
195201
# from being completely empty
196202
+ ([''] if '' in ok_records else []))})
203+
if request.method == 'POST':
204+
for f in ok_filters:
205+
try:
206+
lc.action.datastore_delete(
207+
resource_id=resource_id,
208+
filters=f,
209+
)
210+
except ValidationError as e:
211+
if 'constraint_info' in e.error_dict:
212+
error_message = _render_recombinant_constraint_errors(
213+
lc, e, get_chromo(res['name']), 'delete')
214+
h.flash_error(error_message)
215+
# type_ignore_reason: incomplete typing
216+
return record_fail(error_message) # type: ignore
217+
raise
197218

198-
for f in ok_filters:
199-
try:
200-
lc.action.datastore_delete(
201-
resource_id=resource_id,
202-
filters=f,
203-
)
204-
except ValidationError as e:
205-
if 'foreign_constraints' in e.error_dict:
206-
records = []
207-
ok_records = []
208-
chromo = get_chromo(res['name'])
209-
# type_ignore_reason: incomplete typing
210-
error_message = chromo.get('datastore_constraint_errors', {}).get(
211-
'delete', e.error_dict['foreign_constraints'][0]) # type: ignore
212-
# type_ignore_reason: incomplete typing
213-
sql_error_string = e.error_dict['info']['orig'] # type: ignore
214-
error_message = get_constraint_error_from_psql_error(
215-
lc, sql_error_string, error_message)
216-
h.flash_error(error_message)
217-
# type_ignore_reason: incomplete typing
218-
return record_fail(Markup(error_message)) # type: ignore
219-
raise
220-
221-
h.flash_success(_("{num} deleted.").format(num=len(ok_filters)))
219+
h.flash_success(_("{num} deleted.").format(num=len(ok_filters)))
222220

223221
return h.redirect_to(
224222
'recombinant.preview_table',
@@ -609,20 +607,16 @@ def preview_table(resource_name: str,
609607
else:
610608
r = None
611609

612-
errors = session.pop('RECOMBINANT_ERRORS', None)
613-
delete_errors = session.pop('RECOMBINANT_DELETE_ERRORS', None)
614-
filters = session.pop('RECOMBINANT_FILTERS', None)
615-
616610
return render('recombinant/resource_edit.html', extra_vars={
617611
'dataset': dataset,
618612
'dataset_type': chromo['dataset_type'],
619613
'resource_description': chromo['title'],
620614
'resource_name': chromo['resource_name'],
621615
'resource': r,
622616
'organization': org,
623-
'errors': [errors] if errors else None,
624-
'delete_errors': [delete_errors] if delete_errors else None,
625-
'filters': filters
617+
'errors': None,
618+
'delete_errors': None,
619+
'filters': None
626620
})
627621

628622

@@ -736,7 +730,11 @@ def _process_upload_file(lc: LocalCKAN,
736730
context={'connection': ds_write_connection}
737731
)
738732
except ValidationError as e:
739-
if 'info' in e.error_dict:
733+
if 'constraint_info' in e.error_dict:
734+
# type_ignore_reason: incomplete typing
735+
pgerror = e.error_dict['errors'][
736+
'foreign_constraint'][0] # type: ignore
737+
elif 'info' in e.error_dict:
740738
# because, where else would you put the error text?
741739
# XXX improve this in datastore, please
742740
# type_ignore_reason: incomplete typing
@@ -758,15 +756,9 @@ def _process_upload_file(lc: LocalCKAN,
758756
pgerror = re.sub(r'\nLINE \d+:', '', pgerror)
759757
pgerror = re.sub(r'\n *\^\n$', '', pgerror)
760758
if 'records_row' in e.error_dict:
761-
if 'violates foreign key constraint' in pgerror:
762-
foreign_error = chromo.get(
763-
'datastore_constraint_errors', {}).get('upsert')
764-
# type_ignore_reason: incomplete typing
765-
sql_error_string = \
766-
e.error_dict['upsert_info']['orig'] # type: ignore
767-
if foreign_error:
768-
pgerror = get_constraint_error_from_psql_error(
769-
lc, sql_error_string, foreign_error)
759+
if 'constraint_info' in e.error_dict:
760+
pgerror = _render_recombinant_constraint_errors(
761+
lc, e, chromo, 'upsert')
770762
elif 'invalid input syntax for type integer' in pgerror:
771763
if ':' in pgerror:
772764
pgerror = _('Invalid input syntax for type integer: {}')\
@@ -794,3 +786,43 @@ def _process_upload_file(lc: LocalCKAN,
794786
raise
795787
finally:
796788
ds_write_connection.close()
789+
790+
791+
def _render_recombinant_constraint_errors(lc: LocalCKAN,
792+
exception: Exception,
793+
chromo: Dict[str, Any],
794+
action: str) -> str:
795+
# type_ignore_reason: incomplete typing
796+
orig_errmsg = exception.error_dict['errors'][
797+
'foreign_constraint'][0] # type: ignore
798+
foreign_error = chromo.get(
799+
'datastore_constraint_errors', {}).get(action)
800+
fk_err_template = chromo.get(
801+
'datastore_constraint_error_templates', {}).get(action)
802+
if foreign_error and not fk_err_template:
803+
# type_ignore_reason: incomplete typing
804+
error_message = _parse_constraint_error_from_psql_error(
805+
exception, foreign_error)['errors'][
806+
'foreign_constraint'][0] # type: ignore
807+
elif fk_err_template:
808+
ref_res_dict = lc.action.resource_show(
809+
id=exception.error_dict['constraint_info']['ref_resource'])
810+
ref_pkg_dict = lc.action.package_show(
811+
id=ref_res_dict['package_id'])
812+
dt_query = {}
813+
_ref_keys = exception.error_dict['constraint_info'][
814+
'ref_keys'].replace(' ', '').split(',')
815+
_ref_values = exception.error_dict['constraint_info'][
816+
'ref_values'].replace(' ', '').split(',')
817+
for _i, key in enumerate(_ref_keys):
818+
dt_query[key] = _ref_values[_i]
819+
dt_query = json.dumps(dt_query, separators=(',', ':'))
820+
error_message = render(fk_err_template,
821+
extra_vars=dict(
822+
exception.error_dict['constraint_info'],
823+
ref_resource=ref_res_dict,
824+
ref_dataset=ref_pkg_dict,
825+
dt_query=dt_query))
826+
else:
827+
error_message = orig_errmsg
828+
return error_message

0 commit comments

Comments
 (0)