@@ -161,6 +161,27 @@ def test_new_election_hides_start_button_until_saved(self) -> None:
161161 self .assertNotContains (resp , 'data-target="#start-election-modal"' )
162162 self .assertNotContains (resp , 'id="start-election-modal"' )
163163
164+ def test_new_election_save_draft_button_allows_invalid_submit_without_formnovalidate (self ) -> None :
165+ self ._login_as_freeipa_user ("admin" )
166+ FreeIPAPermissionGrant .objects .create (
167+ principal_type = FreeIPAPermissionGrant .PrincipalType .user ,
168+ principal_name = "admin" ,
169+ permission = ASTRA_ADD_ELECTION ,
170+ )
171+
172+ resp = self .client .get (reverse ("election-edit" , args = [0 ]))
173+ self .assertEqual (resp .status_code , 200 )
174+
175+ html = resp .content .decode ("utf-8" )
176+ self .assertRegex (
177+ html ,
178+ r"<button[^>]*data-allow-invalid-submit=\"true\"[^>]*title=\"Save changes as a draft\"[^>]*>" ,
179+ )
180+ self .assertNotRegex (
181+ html ,
182+ r"<button[^>]*formnovalidate[^>]*title=\"Save changes as a draft\"[^>]*>" ,
183+ )
184+
164185
165186class ElectionDraftDeletionTests (TestCase ):
166187 def _login_as_freeipa_user (self , username : str ) -> None :
@@ -249,7 +270,7 @@ def test_new_election_post_save_draft_creates_election(self) -> None:
249270 self .assertEqual (resp .status_code , 302 )
250271 self .assertEqual (Election .objects .filter (name = "New draft" , status = Election .Status .draft ).count (), 1 )
251272
252- def test_new_election_post_rejects_zero_seats (self ) -> None :
273+ def test_start_election_submit_button_allows_invalid_submit_for_server_validation (self ) -> None :
253274 self ._login_as_freeipa_user ("admin" )
254275 FreeIPAPermissionGrant .objects .create (
255276 principal_type = FreeIPAPermissionGrant .PrincipalType .user ,
@@ -258,17 +279,91 @@ def test_new_election_post_rejects_zero_seats(self) -> None:
258279 )
259280
260281 now = timezone .now ()
282+ election = Election .objects .create (
283+ name = "Draft election" ,
284+ description = "" ,
285+ url = "" ,
286+ start_datetime = now + datetime .timedelta (days = 10 ),
287+ end_datetime = now + datetime .timedelta (days = 11 ),
288+ number_of_seats = 1 ,
289+ status = Election .Status .draft ,
290+ )
291+
292+ resp = self .client .get (reverse ("election-edit" , args = [election .id ]))
293+ self .assertEqual (resp .status_code , 200 )
294+ html = resp .content .decode ("utf-8" )
295+ self .assertRegex (
296+ html ,
297+ r"<button(?=[^>]*title=\"Start the election and send credentials\")(?=[^>]*data-allow-invalid-submit=\"true\")[^>]*>" ,
298+ )
299+ self .assertNotRegex (
300+ html ,
301+ r"<button[^>]*title=\"Start the election and send credentials\"[^>]*formnovalidate[^>]*>" ,
302+ )
303+
304+ def test_start_election_without_candidates_shows_specific_error_without_generic_duplicate (self ) -> None :
305+ self ._login_as_freeipa_user ("admin" )
306+ FreeIPAPermissionGrant .objects .create (
307+ principal_type = FreeIPAPermissionGrant .PrincipalType .user ,
308+ principal_name = "admin" ,
309+ permission = ASTRA_ADD_ELECTION ,
310+ )
311+
312+ now = timezone .now ()
313+ election = Election .objects .create (
314+ name = "Draft election" ,
315+ description = "" ,
316+ url = "" ,
317+ start_datetime = now + datetime .timedelta (days = 10 ),
318+ end_datetime = now + datetime .timedelta (days = 11 ),
319+ number_of_seats = 1 ,
320+ quorum = 10 ,
321+ status = Election .Status .draft ,
322+ )
323+
324+ resp = self .client .post (
325+ reverse ("election-edit" , args = [election .id ]),
326+ data = {
327+ "action" : "start_election" ,
328+ "name" : election .name ,
329+ "description" : election .description ,
330+ "url" : election .url ,
331+ "start_datetime" : election .start_datetime .strftime ("%Y-%m-%dT%H:%M" ),
332+ "end_datetime" : election .end_datetime .strftime ("%Y-%m-%dT%H:%M" ),
333+ "number_of_seats" : str (election .number_of_seats ),
334+ "quorum" : str (election .quorum ),
335+ "email_template_id" : "" ,
336+ "subject" : "" ,
337+ "html_content" : "" ,
338+ "text_content" : "" ,
339+ },
340+ follow = False ,
341+ )
342+
343+ self .assertEqual (resp .status_code , 200 )
344+ self .assertContains (resp , "Add at least one candidate before starting the election." )
345+ self .assertNotContains (resp , "Please correct the errors below." )
346+
347+ def test_new_election_post_save_draft_with_only_name_creates_partial_election (self ) -> None :
348+ self ._login_as_freeipa_user ("admin" )
349+ FreeIPAPermissionGrant .objects .create (
350+ principal_type = FreeIPAPermissionGrant .PrincipalType .user ,
351+ principal_name = "admin" ,
352+ permission = ASTRA_ADD_ELECTION ,
353+ )
354+
261355 resp = self .client .post (
262356 reverse ("election-edit" , args = [0 ]),
263357 data = {
264358 "action" : "save_draft" ,
265- "name" : "Invalid draft" ,
359+ "name" : "Partial draft" ,
266360 "description" : "" ,
267361 "url" : "" ,
268- "start_datetime" : (now + datetime .timedelta (days = 10 )).strftime ("%Y-%m-%dT%H:%M" ),
269- "end_datetime" : (now + datetime .timedelta (days = 11 )).strftime ("%Y-%m-%dT%H:%M" ),
270- "number_of_seats" : "0" ,
271- "quorum" : "50" ,
362+ "start_datetime" : "" ,
363+ "end_datetime" : "" ,
364+ "number_of_seats" : "" ,
365+ "quorum" : "" ,
366+ "eligible_group_cn" : "" ,
272367 "email_template_id" : "" ,
273368 "subject" : "" ,
274369 "html_content" : "" ,
@@ -291,12 +386,78 @@ def test_new_election_post_rejects_zero_seats(self) -> None:
291386 follow = False ,
292387 )
293388
294- self .assertEqual (resp .status_code , 200 )
295- self .assertContains (resp , "Number of seats" )
296- self .assertContains (resp , "Ensure this value is greater than or equal to 1" )
297- self .assertContains (resp , 'id="election-edit-form"' )
298- self .assertContains (resp , "was-validated" )
299- self .assertContains (resp , "invalid-feedback" )
389+ self .assertEqual (resp .status_code , 302 )
390+ election = Election .objects .get (name = "Partial draft" )
391+ self .assertEqual (election .status , Election .Status .draft )
392+ self .assertEqual (election .start_datetime , election .end_datetime )
393+
394+ def test_start_election_still_requires_required_fields_after_partial_draft_save (self ) -> None :
395+ self ._login_as_freeipa_user ("admin" )
396+ FreeIPAPermissionGrant .objects .create (
397+ principal_type = FreeIPAPermissionGrant .PrincipalType .user ,
398+ principal_name = "admin" ,
399+ permission = ASTRA_ADD_ELECTION ,
400+ )
401+
402+ save_resp = self .client .post (
403+ reverse ("election-edit" , args = [0 ]),
404+ data = {
405+ "action" : "save_draft" ,
406+ "name" : "Draft requiring completion" ,
407+ "description" : "" ,
408+ "url" : "" ,
409+ "start_datetime" : "" ,
410+ "end_datetime" : "" ,
411+ "number_of_seats" : "" ,
412+ "quorum" : "" ,
413+ "eligible_group_cn" : "" ,
414+ "email_template_id" : "" ,
415+ "subject" : "" ,
416+ "html_content" : "" ,
417+ "text_content" : "" ,
418+ "candidates-TOTAL_FORMS" : "1" ,
419+ "candidates-INITIAL_FORMS" : "0" ,
420+ "candidates-MIN_NUM_FORMS" : "0" ,
421+ "candidates-MAX_NUM_FORMS" : "1000" ,
422+ "candidates-0-id" : "" ,
423+ "candidates-0-freeipa_username" : "" ,
424+ "candidates-0-nominated_by" : "" ,
425+ "candidates-0-description" : "" ,
426+ "candidates-0-url" : "" ,
427+ "candidates-0-DELETE" : "" ,
428+ "groups-TOTAL_FORMS" : "0" ,
429+ "groups-INITIAL_FORMS" : "0" ,
430+ "groups-MIN_NUM_FORMS" : "0" ,
431+ "groups-MAX_NUM_FORMS" : "1000" ,
432+ },
433+ follow = False ,
434+ )
435+ self .assertEqual (save_resp .status_code , 302 )
436+ election = Election .objects .get (name = "Draft requiring completion" )
437+
438+ start_resp = self .client .post (
439+ reverse ("election-edit" , args = [election .id ]),
440+ data = {
441+ "action" : "start_election" ,
442+ "name" : election .name ,
443+ "description" : election .description ,
444+ "url" : election .url ,
445+ "start_datetime" : "" ,
446+ "end_datetime" : "" ,
447+ "number_of_seats" : "" ,
448+ "quorum" : "" ,
449+ "email_template_id" : "" ,
450+ "subject" : "" ,
451+ "html_content" : "" ,
452+ "text_content" : "" ,
453+ },
454+ follow = False ,
455+ )
456+
457+ self .assertEqual (start_resp .status_code , 200 )
458+ self .assertContains (start_resp , "Please correct the errors below." )
459+ election .refresh_from_db ()
460+ self .assertEqual (election .status , Election .Status .draft )
300461
301462 @override_settings (ELECTION_ELIGIBILITY_MIN_MEMBERSHIP_AGE_DAYS = 1 )
302463 def test_save_draft_rejects_self_nomination_and_does_not_save_candidate (self ) -> None :
0 commit comments