22from flask_babel import force_locale
33import re
44import simplejson as json
5- from markupsafe import Markup
65from 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
98from ckan .types import Response
109
1110from werkzeug .datastructures import FileStorage as FlaskFileStorage
4645from ckanext .recombinant .tables import get_chromo , get_geno
4746from 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
5149from io import BytesIO
5250
5553from ckanext .datastore .backend import DatastoreBackend
5654from 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
6160from 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' ])
112110def 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