@@ -47,7 +47,7 @@ class Challenge(dict):
47
47
"type" , "extra" , "image" , "protocol" , "host" ,
48
48
"connection_info" , "healthcheck" , "attempts" , "flags" ,
49
49
"files" , "topics" , "tags" , "files" , "hints" ,
50
- "requirements" , "state" , "version" ,
50
+ "requirements" , "next_id" , " state" , "version" ,
51
51
# fmt: on
52
52
]
53
53
@@ -103,6 +103,8 @@ def is_default_challenge_property(key: str, value: Any) -> bool:
103
103
if key in ["tags" , "hints" , "topics" , "requirements" , "files" ] and value == []:
104
104
return True
105
105
106
+ if key == "next_id" and value is None :
107
+ return True
106
108
return False
107
109
108
110
@staticmethod
@@ -434,6 +436,40 @@ def _set_required_challenges(self):
434
436
r = self .api .patch (f"/api/v1/challenges/{ self .challenge_id } " , json = requirements_payload )
435
437
r .raise_for_status ()
436
438
439
+ def _set_next_id (self , nid ):
440
+ if type (nid ) == str :
441
+ # nid by name
442
+ # find the challenge id from installed challenges
443
+ remote_challenges = self .load_installed_challenges ()
444
+ for remote_challenge in remote_challenges :
445
+ if remote_challenge ["name" ] == nid :
446
+ nid = remote_challenge ["id" ]
447
+ break
448
+ if type (nid ) == str :
449
+ click .secho (
450
+ "Challenge cannot find next_id. Maybe it is invalid name or id. It will be cleared." ,
451
+ fg = "yellow" ,
452
+ )
453
+ nid = None
454
+ elif type (nid ) == int and nid > 0 :
455
+ # nid by challenge id
456
+ # trust it and use it directly
457
+ nid = remote_challenge ["id" ]
458
+ else :
459
+ nid = None
460
+
461
+ if self .challenge_id == nid :
462
+ click .secho (
463
+ "Challenge cannot set next_id itself. Skipping invalid next_id." ,
464
+ fg = "yellow" ,
465
+ )
466
+ nid = None
467
+
468
+ #return nid
469
+ next_id_payload = {"next_id" : nid }
470
+ r = self .api .patch (f"/api/v1/challenges/{ self .challenge_id } " , json = next_id_payload )
471
+ r .raise_for_status ()
472
+
437
473
# Compare challenge requirements, will resolve all IDs to names
438
474
def _compare_challenge_requirements (self , r1 : List [Union [str , int ]], r2 : List [Union [str , int ]]) -> bool :
439
475
remote_challenges = self .load_installed_challenges ()
@@ -453,6 +489,21 @@ def normalize_requirements(requirements):
453
489
454
490
return normalize_requirements (r1 ) == normalize_requirements (r2 )
455
491
492
+ # Compare challenge next_id, will resolve all IDs to names
493
+ def _compare_challenge_next_id (self , r1 : Union [str , int , None ], r2 : Union [str , int , None ]) -> bool :
494
+ def normalize_next_id (r ):
495
+ normalized = None
496
+ if type (r ) == int :
497
+ remote_challenge = self .load_installed_challenge (r )
498
+ if remote_challenge ["id" ] == r :
499
+ normalized = remote_challenge ["name" ]
500
+ else :
501
+ normalized = r
502
+
503
+ return normalized
504
+
505
+ return normalize_next_id (r1 ) == normalize_next_id (r2 )
506
+
456
507
# Normalize challenge data from the API response to match challenge.yml
457
508
# It will remove any extra fields from the remote, as well as expand external references
458
509
# that have to be fetched separately (e.g., files, flags, hints, etc.)
@@ -521,6 +572,16 @@ def _normalize_challenge(self, challenge_data: Dict[str, Any]):
521
572
challenges = r .json ()["data" ]
522
573
challenge ["requirements" ] = [c ["name" ] for c in challenges if c ["id" ] in requirements ]
523
574
575
+ # Add next_id
576
+ nid = challenge_data .get ("next_id" , None )
577
+ if nid :
578
+ # Prefer challenge names over IDs
579
+ r = self .api .get (f"/api/v1/challenges/{ nid } " )
580
+ r .raise_for_status ()
581
+ challenge ["next_id" ] = r .json ()["data" ]["name" ]
582
+ else :
583
+ challenge ["next_id" ] = None
584
+
524
585
return challenge
525
586
526
587
# Create a dictionary of remote files in { basename: {"url": "", "location": ""} } format
@@ -634,6 +695,11 @@ def sync(self, ignore: Tuple[str] = ()) -> None:
634
695
if challenge .get ("requirements" ) and "requirements" not in ignore :
635
696
self ._set_required_challenges ()
636
697
698
+ # Set next_id
699
+ nid = challenge .get ("next_id" , None )
700
+ if "next_id" not in ignore :
701
+ self ._set_next_id (nid )
702
+
637
703
make_challenge_visible = False
638
704
639
705
# Bring back the challenge to be visible if:
@@ -711,6 +777,11 @@ def create(self, ignore: Tuple[str] = ()) -> None:
711
777
if challenge .get ("requirements" ) and "requirements" not in ignore :
712
778
self ._set_required_challenges ()
713
779
780
+ # Add next_id
781
+ nid = challenge .get ("next_id" , None )
782
+ if "next_id" not in ignore :
783
+ self ._set_next_id (nid )
784
+
714
785
# Bring back the challenge if it's supposed to be visible
715
786
# Either explicitly, or by assuming the default value (possibly because the state is ignored)
716
787
if challenge .get ("state" , "visible" ) == "visible" or "state" in ignore :
@@ -864,6 +935,9 @@ def verify(self, ignore: Tuple[str] = ()) -> bool:
864
935
if key == "requirements" :
865
936
if self ._compare_challenge_requirements (challenge [key ], normalized_challenge [key ]):
866
937
continue
938
+ if key == "next_id" :
939
+ if self ._compare_challenge_next_id (challenge [key ], normalized_challenge [key ]):
940
+ continue
867
941
868
942
return False
869
943
0 commit comments