22from flask_babel import force_locale
33import re
44import simplejson as json
5+ from sqlalchemy import text as sql_text
56
6- from typing import Union , Optional , Dict , Tuple , Any
7+ from typing import Union , Dict , Tuple , Any , List
78from ckan .types import Response
89
910from werkzeug .datastructures import FileStorage as FlaskFileStorage
4647
4748from io import BytesIO
4849
50+ # use import incase of ckan.datastore.sqlsearch.enabled = False
51+ from ckanext .datastore .logic .action import datastore_search_sql
4952from ckanext .datastore .backend import DatastoreBackend
50- from ckanext .datastore .backend .postgres import DatastorePostgresqlBackend
53+ from ckanext .datastore .backend .postgres import (
54+ DatastorePostgresqlBackend ,
55+ identifier as sql_identifier ,
56+ _parse_constraint_error_from_psql_error
57+ )
5158
5259from ckanapi import NotFound , LocalCKAN
5360
@@ -98,7 +105,7 @@ def upload(id: str) -> Response:
98105 owner_org = org ['name' ])
99106
100107
101- @recombinant .route ('/recombinant/delete/<id>/<resource_id>' , methods = ['POST' ])
108+ @recombinant .route ('/recombinant/delete/<id>/<resource_id>' , methods = ['GET' , ' POST' ])
102109def delete_records (id : str , resource_id : str ) -> Union [str , Response ]:
103110 lc = LocalCKAN (username = g .user )
104111 filters = {}
@@ -116,19 +123,26 @@ def delete_records(id: str, resource_id: str) -> Union[str, Response]:
116123 dataset = lc .action .recombinant_show (
117124 dataset_type = pkg ['type' ], owner_org = org ['name' ])
118125
119- def delete_error (err : str ) -> str :
120- return render ('recombinant/resource_edit.html' ,
121- extra_vars = {'delete_errors' : [err ],
122- 'dataset' : dataset ,
123- 'dataset_type' : dataset ['dataset_type' ],
126+ # type_ignore_reason: [] default is allowed
127+ def delete_error (err : str , _records : List [str ] = []) -> str : # type: ignore
128+ return render ('recombinant/confirm_delete.html' ,
129+ extra_vars = {'dataset' : dataset ,
124130 'resource' : res ,
125- 'organization' : org ,
126- 'filters' : filters ,
127- 'action' : 'edit' })
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 []))})
128138
129139 form_text = request .form .get ('bulk-delete' , '' )
130140 if not form_text :
131- 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' ])
132146
133147 pk_fields = recombinant_primary_key_fields (res ['name' ])
134148
@@ -142,7 +156,7 @@ def record_fail(err: str) -> str:
142156 # move bad record to the top of the pile
143157 filters ['bulk-delete' ] = '\n ' .join (
144158 [r ] + list (records ) + ok_records )
145- return delete_error (err )
159+ return delete_error (err , ok_records )
146160
147161 split_on = '\t ' if '\t ' in r else ','
148162 fields = [f for f in r .split (split_on )]
@@ -175,8 +189,8 @@ def record_fail(err: str) -> str:
175189 return h .redirect_to (
176190 'recombinant.preview_table' ,
177191 resource_name = res ['name' ],
178- owner_org = org ['name' ], )
179- if 'confirm' not in request .form :
192+ owner_org = org ['name' ])
193+ if 'confirm' not in request .form or request . method == 'GET' :
180194 return render ('recombinant/confirm_delete.html' ,
181195 extra_vars = {'dataset' : dataset ,
182196 'resource' : res ,
@@ -186,27 +200,23 @@ def record_fail(err: str) -> str:
186200 # extra blank is needed to prevent field
187201 # from being completely empty
188202 + (['' ] 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
189218
190- for f in ok_filters :
191- try :
192- lc .action .datastore_delete (
193- resource_id = resource_id ,
194- filters = f ,
195- )
196- except ValidationError as e :
197- if 'foreign_constraints' in e .error_dict :
198- records = []
199- ok_records = []
200- chromo = get_chromo (res ['name' ])
201- # type_ignore_reason: incomplete typing
202- error_message = chromo .get ('datastore_constraint_errors' , {}).get (
203- 'delete' , e .error_dict ['foreign_constraints' ][0 ]) # type: ignore
204- h .flash_error (_ (error_message ))
205- # type_ignore_reason: incomplete typing
206- return record_fail (_ (error_message )) # type: ignore
207- raise
208-
209- h .flash_success (_ ("{num} deleted." ).format (num = len (ok_filters )))
219+ h .flash_success (_ ("{num} deleted." ).format (num = len (ok_filters )))
210220
211221 return h .redirect_to (
212222 'recombinant.preview_table' ,
@@ -299,6 +309,44 @@ def template(dataset_type: str, lang: str, owner_org: str) -> Response:
299309
300310 append_data (book , record_data , chromo )
301311
312+ resource_names = dict ((r ['id' ], r ['name' ]) for r in dataset ['resources' ])
313+ ds_info = lc .action .datastore_info (id = resource ['id' ])
314+ if 'foreignkeys' in ds_info ['meta' ]:
315+ for fk in ds_info ['meta' ]['foreignkeys' ]:
316+ f_chromo = None
317+ foreign_constraints_sql = None
318+ if resource ['id' ] == fk ['child_table' ]:
319+ f_chromo = get_chromo (resource_names [fk ['parent_table' ]])
320+ foreign_constraints_sql = sql_text ('''
321+ SELECT parent.* FROM {0} parent
322+ JOIN {1} child ON {2}
323+ ORDER BY parent._id
324+ ''' .format (sql_identifier (fk ['parent_table' ]),
325+ sql_identifier (fk ['child_table' ]),
326+ ' AND ' .join (
327+ ['parent.{0} = child.{1}' .format (
328+ fk_c , fk ['child_columns' ][fk_i ])
329+ for fk_i , fk_c in enumerate (
330+ fk ['parent_columns' ])])))
331+ elif resource ['id' ] == fk ['parent_table' ]:
332+ f_chromo = get_chromo (resource_names [fk ['child_table' ]])
333+ foreign_constraints_sql = sql_text ('''
334+ SELECT child.* FROM {0} child
335+ JOIN {1} parent ON {2}
336+ ORDER BY child._id
337+ ''' .format (sql_identifier (fk ['child_table' ]),
338+ sql_identifier (fk ['parent_table' ]),
339+ ' AND ' .join (
340+ ['child.{0} = parent.{1}' .format (
341+ fk_c , fk ['parent_columns' ][fk_i ])
342+ for fk_i , fk_c in enumerate (
343+ fk ['child_columns' ])])))
344+ if foreign_constraints_sql is not None and f_chromo is not None :
345+ results = datastore_search_sql (
346+ {'ignore_auth' : True }, {'sql' : str (foreign_constraints_sql )})
347+ if results :
348+ append_data (book , results ['records' ], f_chromo )
349+
302350 blob = BytesIO ()
303351 book .save (blob )
304352 response = FlaskResponse (blob .getvalue ())
@@ -490,8 +538,7 @@ def resource_redirect(package_type: str, id: str, resource_id: str) -> Response:
490538
491539@recombinant .route ('/recombinant/<resource_name>/<owner_org>' , methods = ['GET' , 'POST' ])
492540def preview_table (resource_name : str ,
493- owner_org : str ,
494- errors : Optional [Dict [str , Any ]] = None ) -> Union [str , Response ]:
541+ owner_org : str ) -> Union [str , Response ]:
495542 if not g .user :
496543 return h .redirect_to ('user.login' )
497544
@@ -576,7 +623,9 @@ def preview_table(resource_name: str,
576623 'resource_name' : chromo ['resource_name' ],
577624 'resource' : r ,
578625 'organization' : org ,
579- 'errors' : errors ,
626+ 'errors' : None ,
627+ 'delete_errors' : None ,
628+ 'filters' : None
580629 })
581630
582631
@@ -690,7 +739,11 @@ def _process_upload_file(lc: LocalCKAN,
690739 context = {'connection' : ds_write_connection }
691740 )
692741 except ValidationError as e :
693- if 'info' in e .error_dict :
742+ if 'constraint_info' in e .error_dict :
743+ # type_ignore_reason: incomplete typing
744+ pgerror = e .error_dict ['errors' ][ # type: ignore
745+ 'foreign_constraint' ][0 ]
746+ elif 'info' in e .error_dict :
694747 # because, where else would you put the error text?
695748 # XXX improve this in datastore, please
696749 # type_ignore_reason: incomplete typing
@@ -712,11 +765,9 @@ def _process_upload_file(lc: LocalCKAN,
712765 pgerror = re .sub (r'\nLINE \d+:' , '' , pgerror )
713766 pgerror = re .sub (r'\n *\^\n$' , '' , pgerror )
714767 if 'records_row' in e .error_dict :
715- if 'violates foreign key constraint' in pgerror :
716- foreign_error = chromo .get (
717- 'datastore_constraint_errors' , {}).get ('upsert' )
718- if foreign_error :
719- pgerror = _ (foreign_error )
768+ if 'constraint_info' in e .error_dict :
769+ pgerror = _render_recombinant_constraint_errors (
770+ lc , e , chromo , 'upsert' )
720771 elif 'invalid input syntax for type integer' in pgerror :
721772 if ':' in pgerror :
722773 pgerror = _ ('Invalid input syntax for type integer: {}' )\
@@ -744,3 +795,47 @@ def _process_upload_file(lc: LocalCKAN,
744795 raise
745796 finally :
746797 ds_write_connection .close ()
798+
799+
800+ def _render_recombinant_constraint_errors (lc : LocalCKAN ,
801+ exception : Exception ,
802+ chromo : Dict [str , Any ],
803+ action : str ) -> str :
804+ # type_ignore_reason: incomplete typing
805+ orig_errmsg = exception .error_dict ['errors' ][ # type: ignore
806+ 'foreign_constraint' ][0 ]
807+ foreign_error = chromo .get (
808+ 'datastore_constraint_errors' , {}).get (action )
809+ fk_err_template = chromo .get (
810+ 'datastore_constraint_error_templates' , {}).get (action )
811+ if foreign_error and not fk_err_template :
812+ error_message = _parse_constraint_error_from_psql_error (
813+ exception , foreign_error )['errors' ][
814+ 'foreign_constraint' ][0 ]
815+ elif fk_err_template :
816+ ref_res_dict = lc .action .resource_show (
817+ # type_ignore_reason: incomplete typing
818+ id = exception .error_dict ['constraint_info' ]['ref_resource' ]) # type: ignore
819+ ref_pkg_dict = lc .action .package_show (
820+ id = ref_res_dict ['package_id' ])
821+ dt_query = {}
822+ # type_ignore_reason: incomplete typing
823+ _ref_keys = exception .error_dict ['constraint_info' ][ # type: ignore
824+ 'ref_keys' ].replace (' ' , '' ).split (',' )
825+ # type_ignore_reason: incomplete typing
826+ _ref_values = exception .error_dict ['constraint_info' ][ # type: ignore
827+ 'ref_values' ].replace (' ' , '' ).split (',' )
828+ for _i , key in enumerate (_ref_keys ):
829+ dt_query [key ] = _ref_values [_i ]
830+ dt_query = json .dumps (dt_query , separators = (',' , ':' ))
831+ error_message = render (
832+ fk_err_template ,
833+ extra_vars = dict (
834+ # type_ignore_reason: incomplete typing
835+ exception .error_dict ['constraint_info' ], # type: ignore
836+ ref_resource = ref_res_dict ,
837+ ref_dataset = ref_pkg_dict ,
838+ dt_query = dt_query ))
839+ else :
840+ error_message = orig_errmsg
841+ return error_message
0 commit comments