@@ -781,3 +781,326 @@ def test_create_template_card_with_invalid_scheduled_value(self, api_client, sam
781781 assert 'Scheduled must be a boolean' in response .json ()['message' ]
782782
783783
784+ @pytest .mark .api
785+ class TestColumnSpecificDuplicateChecking :
786+ """Test cases for column-specific duplicate checking behavior in card scheduler."""
787+
788+ def test_schedule_allows_cards_in_different_columns (self , api_client , sample_board ):
789+ """Test that allow_duplicates=False allows cards in different columns."""
790+ # Create two columns
791+ col1_response = requests .post (f"{ api_client } /api/boards/{ sample_board ['id' ]} /columns" , json = {
792+ 'name' : 'Column 1'
793+ })
794+ assert col1_response .status_code == 201
795+ col1 = col1_response .json ()['column' ]
796+
797+ col2_response = requests .post (f"{ api_client } /api/boards/{ sample_board ['id' ]} /columns" , json = {
798+ 'name' : 'Column 2'
799+ })
800+ assert col2_response .status_code == 201
801+ col2 = col2_response .json ()['column' ]
802+
803+ # Create template card in column 1
804+ card_response = requests .post (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" , json = {
805+ 'title' : 'Multi-Column Template' ,
806+ 'description' : 'Can exist in multiple columns'
807+ })
808+ assert card_response .status_code == 201
809+ template_card = card_response .json ()['card' ]
810+
811+ # Create schedule with allow_duplicates=False
812+ now = datetime .now ()
813+ schedule_response = requests .post (f"{ api_client } /api/schedules" , json = {
814+ 'card_id' : template_card ['id' ],
815+ 'run_every' : 1 ,
816+ 'unit' : 'day' ,
817+ 'start_datetime' : (now - timedelta (minutes = 5 )).strftime ('%Y-%m-%dT%H:%M:%S' ),
818+ 'schedule_enabled' : True ,
819+ 'allow_duplicates' : False ,
820+ 'keep_source_card' : False
821+ })
822+ assert schedule_response .status_code == 201
823+ schedule = schedule_response .json ()['schedule' ]
824+
825+ # Manually create a card from this schedule in column 1 (simulates scheduler creating it)
826+ card1_response = requests .post (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" , json = {
827+ 'title' : 'Multi-Column Template' ,
828+ 'description' : 'Can exist in multiple columns' ,
829+ 'schedule' : schedule ['id' ]
830+ })
831+ assert card1_response .status_code == 201
832+ card1 = card1_response .json ()['card' ]
833+
834+ # Manually create a card from this schedule in column 2
835+ card2_response = requests .post (f"{ api_client } /api/columns/{ col2 ['id' ]} /cards" , json = {
836+ 'title' : 'Multi-Column Template' ,
837+ 'description' : 'Can exist in multiple columns' ,
838+ 'schedule' : schedule ['id' ]
839+ })
840+ assert card2_response .status_code == 201
841+ card2 = card2_response .json ()['card' ]
842+
843+ # Verify both cards exist and are not archived
844+ cards1_response = requests .get (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" )
845+ cards1 = cards1_response .json ()['cards' ]
846+ assert len ([c for c in cards1 if c ['id' ] == card1 ['id' ]]) == 1
847+
848+ cards2_response = requests .get (f"{ api_client } /api/columns/{ col2 ['id' ]} /cards" )
849+ cards2 = cards2_response .json ()['cards' ]
850+ assert len ([c for c in cards2 if c ['id' ] == card2 ['id' ]]) == 1
851+
852+ def test_duplicate_check_prevents_duplicates_in_same_column (self , api_client , sample_board ):
853+ """Test that duplicate check correctly prevents duplicates within the same column."""
854+ # Create column
855+ col_response = requests .post (f"{ api_client } /api/boards/{ sample_board ['id' ]} /columns" , json = {
856+ 'name' : 'Test Column'
857+ })
858+ assert col_response .status_code == 201
859+ column = col_response .json ()['column' ]
860+
861+ # Create template card
862+ card_response = requests .post (f"{ api_client } /api/columns/{ column ['id' ]} /cards" , json = {
863+ 'title' : 'Single Instance Template' ,
864+ 'description' : 'Should only exist once in column'
865+ })
866+ assert card_response .status_code == 201
867+ template_card = card_response .json ()['card' ]
868+
869+ # Create schedule with allow_duplicates=False
870+ now = datetime .now ()
871+ schedule_response = requests .post (f"{ api_client } /api/schedules" , json = {
872+ 'card_id' : template_card ['id' ],
873+ 'run_every' : 1 ,
874+ 'unit' : 'day' ,
875+ 'start_datetime' : (now - timedelta (minutes = 5 )).strftime ('%Y-%m-%dT%H:%M:%S' ),
876+ 'schedule_enabled' : True ,
877+ 'allow_duplicates' : False ,
878+ 'keep_source_card' : False
879+ })
880+ assert schedule_response .status_code == 201
881+ schedule = schedule_response .json ()['schedule' ]
882+
883+ # Create first card from schedule
884+ card1_response = requests .post (f"{ api_client } /api/columns/{ column ['id' ]} /cards" , json = {
885+ 'title' : 'Single Instance Template' ,
886+ 'description' : 'Should only exist once in column' ,
887+ 'schedule' : schedule ['id' ]
888+ })
889+ assert card1_response .status_code == 201
890+ card1 = card1_response .json ()['card' ]
891+
892+ # Verify card exists
893+ cards_response = requests .get (f"{ api_client } /api/columns/{ column ['id' ]} /cards" )
894+ cards = cards_response .json ()['cards' ]
895+ schedule_cards = [c for c in cards if c .get ('schedule' ) == schedule ['id' ]]
896+ assert len (schedule_cards ) == 1
897+ assert schedule_cards [0 ]['id' ] == card1 ['id' ]
898+
899+ # At this point, the scheduler should NOT create a duplicate
900+ # We can't directly test the scheduler logic without triggering it,
901+ # but we've verified the state that would prevent duplication
902+
903+ def test_moved_card_allows_new_card_in_original_column (self , api_client , sample_board ):
904+ """Test that when a card is moved to different column, new card can be created in original column."""
905+ # Create two columns
906+ col1_response = requests .post (f"{ api_client } /api/boards/{ sample_board ['id' ]} /columns" , json = {
907+ 'name' : 'Original Column'
908+ })
909+ assert col1_response .status_code == 201
910+ col1 = col1_response .json ()['column' ]
911+
912+ col2_response = requests .post (f"{ api_client } /api/boards/{ sample_board ['id' ]} /columns" , json = {
913+ 'name' : 'Destination Column'
914+ })
915+ assert col2_response .status_code == 201
916+ col2 = col2_response .json ()['column' ]
917+
918+ # Create template card in column 1
919+ card_response = requests .post (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" , json = {
920+ 'title' : 'Movable Template' ,
921+ 'description' : 'Will be moved between columns'
922+ })
923+ assert card_response .status_code == 201
924+ template_card = card_response .json ()['card' ]
925+
926+ # Create schedule
927+ now = datetime .now ()
928+ schedule_response = requests .post (f"{ api_client } /api/schedules" , json = {
929+ 'card_id' : template_card ['id' ],
930+ 'run_every' : 1 ,
931+ 'unit' : 'day' ,
932+ 'start_datetime' : (now - timedelta (minutes = 5 )).strftime ('%Y-%m-%dT%H:%M:%S' ),
933+ 'schedule_enabled' : True ,
934+ 'allow_duplicates' : False ,
935+ 'keep_source_card' : False
936+ })
937+ assert schedule_response .status_code == 201
938+ schedule = schedule_response .json ()['schedule' ]
939+
940+ # Create card from schedule in column 1
941+ card_response = requests .post (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" , json = {
942+ 'title' : 'Movable Template' ,
943+ 'description' : 'Will be moved between columns' ,
944+ 'schedule' : schedule ['id' ]
945+ })
946+ assert card_response .status_code == 201
947+ created_card = card_response .json ()['card' ]
948+
949+ # Verify card is in column 1
950+ cards1_response = requests .get (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" )
951+ cards1 = cards1_response .json ()['cards' ]
952+ assert len ([c for c in cards1 if c ['id' ] == created_card ['id' ]]) == 1
953+
954+ # Move card to column 2
955+ move_response = requests .patch (f"{ api_client } /api/cards/{ created_card ['id' ]} " , json = {
956+ 'column_id' : col2 ['id' ]
957+ })
958+ assert move_response .status_code == 200
959+
960+ # Verify card is now in column 2
961+ cards2_response = requests .get (f"{ api_client } /api/columns/{ col2 ['id' ]} /cards" )
962+ cards2 = cards2_response .json ()['cards' ]
963+ assert len ([c for c in cards2 if c ['id' ] == created_card ['id' ]]) == 1
964+
965+ # Verify card is no longer in column 1
966+ cards1_response = requests .get (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" )
967+ cards1 = cards1_response .json ()['cards' ]
968+ assert len ([c for c in cards1 if c ['id' ] == created_card ['id' ]]) == 0
969+
970+ # Now column 1 should be available for a new card from the same schedule
971+ # Create another card from schedule in column 1
972+ new_card_response = requests .post (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" , json = {
973+ 'title' : 'Movable Template' ,
974+ 'description' : 'Will be moved between columns' ,
975+ 'schedule' : schedule ['id' ]
976+ })
977+ assert new_card_response .status_code == 201
978+ new_card = new_card_response .json ()['card' ]
979+
980+ # Verify both cards exist in different columns
981+ cards1_response = requests .get (f"{ api_client } /api/columns/{ col1 ['id' ]} /cards" )
982+ cards1 = cards1_response .json ()['cards' ]
983+ assert len ([c for c in cards1 if c ['schedule' ] == schedule ['id' ]]) == 1
984+ assert cards1 [0 ]['id' ] == new_card ['id' ]
985+
986+ cards2_response = requests .get (f"{ api_client } /api/columns/{ col2 ['id' ]} /cards" )
987+ cards2 = cards2_response .json ()['cards' ]
988+ assert len ([c for c in cards2 if c ['schedule' ] == schedule ['id' ]]) == 1
989+ assert cards2 [0 ]['id' ] == created_card ['id' ]
990+
991+ def test_archived_card_allows_new_card_in_same_column (self , api_client , sample_board ):
992+ """Test that archiving a scheduled card allows a new one to be created in the same column."""
993+ # Create column
994+ col_response = requests .post (f"{ api_client } /api/boards/{ sample_board ['id' ]} /columns" , json = {
995+ 'name' : 'Archive Test Column'
996+ })
997+ assert col_response .status_code == 201
998+ column = col_response .json ()['column' ]
999+
1000+ # Create template card
1001+ card_response = requests .post (f"{ api_client } /api/columns/{ column ['id' ]} /cards" , json = {
1002+ 'title' : 'Archivable Template' ,
1003+ 'description' : 'Can be archived and recreated'
1004+ })
1005+ assert card_response .status_code == 201
1006+ template_card = card_response .json ()['card' ]
1007+
1008+ # Create schedule
1009+ now = datetime .now ()
1010+ schedule_response = requests .post (f"{ api_client } /api/schedules" , json = {
1011+ 'card_id' : template_card ['id' ],
1012+ 'run_every' : 1 ,
1013+ 'unit' : 'day' ,
1014+ 'start_datetime' : (now - timedelta (minutes = 5 )).strftime ('%Y-%m-%dT%H:%M:%S' ),
1015+ 'schedule_enabled' : True ,
1016+ 'allow_duplicates' : False ,
1017+ 'keep_source_card' : False
1018+ })
1019+ assert schedule_response .status_code == 201
1020+ schedule = schedule_response .json ()['schedule' ]
1021+
1022+ # Create first card from schedule
1023+ card1_response = requests .post (f"{ api_client } /api/columns/{ column ['id' ]} /cards" , json = {
1024+ 'title' : 'Archivable Template' ,
1025+ 'description' : 'Can be archived and recreated' ,
1026+ 'schedule' : schedule ['id' ]
1027+ })
1028+ assert card1_response .status_code == 201
1029+ card1 = card1_response .json ()['card' ]
1030+
1031+ # Archive the first card
1032+ archive_response = requests .patch (f"{ api_client } /api/cards/{ card1 ['id' ]} " , json = {
1033+ 'archived' : True
1034+ })
1035+ assert archive_response .status_code == 200
1036+
1037+ # Create second card from schedule in same column
1038+ card2_response = requests .post (f"{ api_client } /api/columns/{ column ['id' ]} /cards" , json = {
1039+ 'title' : 'Archivable Template' ,
1040+ 'description' : 'Can be archived and recreated' ,
1041+ 'schedule' : schedule ['id' ]
1042+ })
1043+ assert card2_response .status_code == 201
1044+ card2 = card2_response .json ()['card' ]
1045+
1046+ # Verify only the non-archived card appears in column
1047+ cards_response = requests .get (f"{ api_client } /api/columns/{ column ['id' ]} /cards" )
1048+ cards = cards_response .json ()['cards' ]
1049+ schedule_cards = [c for c in cards if c .get ('schedule' ) == schedule ['id' ]]
1050+ assert len (schedule_cards ) == 1
1051+ assert schedule_cards [0 ]['id' ] == card2 ['id' ]
1052+ assert schedule_cards [0 ]['archived' ] is False
1053+
1054+ def test_allow_duplicates_true_allows_multiple_in_same_column (self , api_client , sample_board ):
1055+ """Test that allow_duplicates=True allows multiple cards in same column."""
1056+ # Create column
1057+ col_response = requests .post (f"{ api_client } /api/boards/{ sample_board ['id' ]} /columns" , json = {
1058+ 'name' : 'Duplicate Allow Column'
1059+ })
1060+ assert col_response .status_code == 201
1061+ column = col_response .json ()['column' ]
1062+
1063+ # Create template card
1064+ card_response = requests .post (f"{ api_client } /api/columns/{ column ['id' ]} /cards" , json = {
1065+ 'title' : 'Duplicate Allowed Template' ,
1066+ 'description' : 'Multiple instances allowed'
1067+ })
1068+ assert card_response .status_code == 201
1069+ template_card = card_response .json ()['card' ]
1070+
1071+ # Create schedule with allow_duplicates=True
1072+ now = datetime .now ()
1073+ schedule_response = requests .post (f"{ api_client } /api/schedules" , json = {
1074+ 'card_id' : template_card ['id' ],
1075+ 'run_every' : 1 ,
1076+ 'unit' : 'hour' ,
1077+ 'start_datetime' : (now - timedelta (hours = 2 )).strftime ('%Y-%m-%dT%H:%M:%S' ),
1078+ 'schedule_enabled' : True ,
1079+ 'allow_duplicates' : True ,
1080+ 'keep_source_card' : False
1081+ })
1082+ assert schedule_response .status_code == 201
1083+ schedule = schedule_response .json ()['schedule' ]
1084+
1085+ # Create multiple cards from same schedule in same column
1086+ created_cards = []
1087+ for i in range (3 ):
1088+ card_resp = requests .post (f"{ api_client } /api/columns/{ column ['id' ]} /cards" , json = {
1089+ 'title' : 'Duplicate Allowed Template' ,
1090+ 'description' : f'Multiple instances allowed - Instance { i + 1 } ' ,
1091+ 'schedule' : schedule ['id' ]
1092+ })
1093+ assert card_resp .status_code == 201
1094+ created_cards .append (card_resp .json ()['card' ])
1095+
1096+ # Verify all cards exist in the column
1097+ cards_response = requests .get (f"{ api_client } /api/columns/{ column ['id' ]} /cards" )
1098+ cards = cards_response .json ()['cards' ]
1099+ schedule_cards = [c for c in cards if c .get ('schedule' ) == schedule ['id' ]]
1100+ assert len (schedule_cards ) == 3
1101+
1102+ # Verify they're all different cards
1103+ card_ids = [c ['id' ] for c in schedule_cards ]
1104+ assert len (set (card_ids )) == 3 # All unique IDs
1105+
1106+
0 commit comments