@@ -145,41 +145,172 @@ def group_move_nodes(source_group, target_group, force, nodes, all_entries):
145145
146146
147147@verdi_group .command ('delete' )
148- @arguments .GROUP ()
148+ @arguments .GROUPS ()
149+ @options .ALL_USERS (help = 'Filter and delete groups for all users, rather than only for the current user.' )
150+ @options .USER (help = 'Add a filter to delete groups belonging to a specific user.' )
151+ @options .TYPE_STRING (help = 'Filter to only include groups of this type string.' )
152+ @options .PAST_DAYS (help = 'Add a filter to delete only groups created in the past N days.' , default = None )
153+ @click .option (
154+ '-s' ,
155+ '--startswith' ,
156+ type = click .STRING ,
157+ default = None ,
158+ help = 'Add a filter to delete only groups for which the label begins with STRING.' ,
159+ )
160+ @click .option (
161+ '-e' ,
162+ '--endswith' ,
163+ type = click .STRING ,
164+ default = None ,
165+ help = 'Add a filter to delete only groups for which the label ends with STRING.' ,
166+ )
167+ @click .option (
168+ '-c' ,
169+ '--contains' ,
170+ type = click .STRING ,
171+ default = None ,
172+ help = 'Add a filter to delete only groups for which the label contains STRING.' ,
173+ )
174+ @options .NODE (help = 'Delete only the groups that contain a node.' )
149175@options .FORCE ()
150176@click .option (
151177 '--delete-nodes' , is_flag = True , default = False , help = 'Delete all nodes in the group along with the group itself.'
152178)
153179@options .graph_traversal_rules (GraphTraversalRules .DELETE .value )
154180@options .DRY_RUN ()
155181@with_dbenv ()
156- def group_delete (group , delete_nodes , dry_run , force , ** traversal_rules ):
157- """Delete a group and (optionally) the nodes it contains."""
182+ def group_delete (
183+ groups ,
184+ delete_nodes ,
185+ dry_run ,
186+ force ,
187+ all_users ,
188+ user ,
189+ type_string ,
190+ past_days ,
191+ startswith ,
192+ endswith ,
193+ contains ,
194+ node ,
195+ ** traversal_rules ,
196+ ):
197+ """Delete groups and (optionally) the nodes they contain."""
198+ from tabulate import tabulate
199+
158200 from aiida import orm
159201 from aiida .tools import delete_group_nodes
160202
161- if not (force or dry_run ):
162- click .confirm (f'Are you sure you want to delete { group } ?' , abort = True )
163- elif dry_run :
164- echo .echo_report (f'Would have deleted { group } .' )
203+ filters_provided = any (
204+ [all_users or user or past_days or startswith or endswith or contains or node or type_string ]
205+ )
206+
207+ if groups and filters_provided :
208+ echo .echo_critical ('Cannot specify both GROUPS and any of the other filters.' )
209+
210+ if not groups and filters_provided :
211+ import datetime
212+
213+ from aiida .common import timezone
214+ from aiida .common .escaping import escape_for_sql_like
215+
216+ builder = orm .QueryBuilder ()
217+ filters = {}
165218
166- if delete_nodes :
219+ # Note: we could have set 'core' as a default value for type_string,
220+ # but for the sake of uniform interface, we decided to keep the default value of None.
221+ # Otherwise `verdi group delete 123 -T core` would have worked, but we say
222+ # 'Cannot specify both GROUPS and any of the other filters'.
223+ if type_string is None :
224+ type_string = 'core'
167225
168- def _dry_run_callback (pks ):
169- if not pks or force :
170- return False
171- echo .echo_warning (f'YOU ARE ABOUT TO DELETE { len (pks )} NODES! THIS CANNOT BE UNDONE!' )
172- return not click .confirm ('Do you want to continue?' , abort = True )
226+ if '%' in type_string or '_' in type_string :
227+ filters ['type_string' ] = {'like' : type_string }
228+ else :
229+ filters ['type_string' ] = type_string
230+
231+ # Creation time
232+ if past_days :
233+ filters ['time' ] = {'>' : timezone .now () - datetime .timedelta (days = past_days )}
234+
235+ # Query for specific group labels
236+ filters ['or' ] = []
237+ if startswith :
238+ filters ['or' ].append ({'label' : {'like' : f'{ escape_for_sql_like (startswith )} %' }})
239+ if endswith :
240+ filters ['or' ].append ({'label' : {'like' : f'%{ escape_for_sql_like (endswith )} ' }})
241+ if contains :
242+ filters ['or' ].append ({'label' : {'like' : f'%{ escape_for_sql_like (contains )} %' }})
243+
244+ builder .append (orm .Group , filters = filters , tag = 'group' , project = '*' )
245+
246+ # Query groups that belong to specific user
247+ if user :
248+ user_email = user .email
249+ else :
250+ # By default: only groups of this user
251+ user_email = orm .User .collection .get_default ().email
173252
174- _ , nodes_deleted = delete_group_nodes ([group .pk ], dry_run = dry_run or _dry_run_callback , ** traversal_rules )
175- if not nodes_deleted :
176- # don't delete the group if the nodes were not deleted
253+ # Query groups that belong to all users
254+ if not all_users :
255+ builder .append (orm .User , filters = {'email' : user_email }, with_group = 'group' )
256+
257+ # Query groups that contain a particular node
258+ if node :
259+ builder .append (orm .Node , filters = {'id' : node .pk }, with_group = 'group' )
260+
261+ groups = builder .all (flat = True )
262+ if not groups :
263+ echo .echo_report ('No groups found matching the specified criteria.' )
177264 return
178265
179- if not dry_run :
266+ elif not groups and not filters_provided :
267+ echo .echo_report ('Nothing happened. Please specify at least one group or provide filters to query groups.' )
268+ return
269+
270+ projection_lambdas = {
271+ 'pk' : lambda group : str (group .pk ),
272+ 'label' : lambda group : group .label ,
273+ 'type_string' : lambda group : group .type_string ,
274+ 'count' : lambda group : group .count (),
275+ 'user' : lambda group : group .user .email .strip (),
276+ 'description' : lambda group : group .description ,
277+ }
278+
279+ table = []
280+ projection_header = ['PK' , 'Label' , 'Type string' , 'User' ]
281+ projection_fields = ['pk' , 'label' , 'type_string' , 'user' ]
282+ for group in groups :
283+ table .append ([projection_lambdas [field ](group ) for field in projection_fields ])
284+
285+ if not (force or dry_run ):
286+ echo .echo_report ('The following groups will be deleted:' )
287+ echo .echo (tabulate (table , headers = projection_header ))
288+ click .confirm ('Are you sure you want to continue?' , abort = True )
289+ elif dry_run :
290+ echo .echo_report ('Would have deleted:' )
291+ echo .echo (tabulate (table , headers = projection_header ))
292+
293+ for group in groups :
180294 group_str = str (group )
181- orm .Group .collection .delete (group .pk )
182- echo .echo_success (f'{ group_str } deleted.' )
295+
296+ if delete_nodes :
297+
298+ def _dry_run_callback (pks ):
299+ if not pks or force :
300+ return False
301+ echo .echo_warning (
302+ f'YOU ARE ABOUT TO DELETE { len (pks )} NODES ASSOCIATED WITH { group_str } ! THIS CANNOT BE UNDONE!'
303+ )
304+ return not click .confirm ('Do you want to continue?' , abort = True )
305+
306+ _ , nodes_deleted = delete_group_nodes ([group .pk ], dry_run = dry_run or _dry_run_callback , ** traversal_rules )
307+ if not nodes_deleted :
308+ # don't delete the group if the nodes were not deleted
309+ return
310+
311+ if not dry_run :
312+ orm .Group .collection .delete (group .pk )
313+ echo .echo_success (f'{ group_str } deleted.' )
183314
184315
185316@verdi_group .command ('relabel' )
@@ -273,7 +404,7 @@ def group_show(group, raw, limit, uuid):
273404@options .ALL_USERS (help = 'Show groups for all users, rather than only for the current user.' )
274405@options .USER (help = 'Add a filter to show only groups belonging to a specific user.' )
275406@options .ALL (help = 'Show groups of all types.' )
276- @options .TYPE_STRING ()
407+ @options .TYPE_STRING (default = 'core' , help = 'Filter to only include groups of this type string.' )
277408@click .option (
278409 '-d' , '--with-description' , 'with_description' , is_flag = True , default = False , help = 'Show also the group description.'
279410)
@@ -302,7 +433,7 @@ def group_show(group, raw, limit, uuid):
302433)
303434@options .ORDER_BY (type = click .Choice (['id' , 'label' , 'ctime' ]), default = 'label' )
304435@options .ORDER_DIRECTION ()
305- @options .NODE (help = 'Show only the groups that contain the node.' )
436+ @options .NODE (help = 'Show only the groups that contain this node.' )
306437@with_dbenv ()
307438def group_list (
308439 all_users ,
@@ -331,12 +462,6 @@ def group_list(
331462 builder = orm .QueryBuilder ()
332463 filters = {}
333464
334- # Have to specify the default for `type_string` here instead of directly in the option otherwise it will always
335- # raise above if the user specifies just the `--group-type` option. Once that option is removed, the default can
336- # be moved to the option itself.
337- if type_string is None :
338- type_string = 'core'
339-
340465 if not all_entries :
341466 if '%' in type_string or '_' in type_string :
342467 filters ['type_string' ] = {'like' : type_string }
@@ -367,11 +492,11 @@ def group_list(
367492
368493 # Query groups that belong to all users
369494 if not all_users :
370- builder .append (orm .User , filters = {'email' : { '==' : user_email } }, with_group = 'group' )
495+ builder .append (orm .User , filters = {'email' : user_email }, with_group = 'group' )
371496
372497 # Query groups that contain a particular node
373498 if node :
374- builder .append (orm .Node , filters = {'id' : { '==' : node .pk } }, with_group = 'group' )
499+ builder .append (orm .Node , filters = {'id' : node .pk }, with_group = 'group' )
375500
376501 builder .order_by ({orm .Group : {order_by : order_dir }})
377502
0 commit comments