From bfd759bdae110df1337ecbfaeb5a494f2a629ff8 Mon Sep 17 00:00:00 2001
From: Julian Rother <julian@cccv.de>
Date: Wed, 20 Apr 2022 16:14:51 +0200
Subject: [PATCH] Fix "new invite" form resetting on error

When the "new invite" page was submitted with e.g. an invalid "Valid Until"
value, uffd displayed an error and reset the whole form. This was confusing
to users.

Now the form content is preserved on errors. Also the "Valid Until" field now
has min/max attributes to prevent submitting the form with invalid values.

Fixes #134
---
 tests/test_invite.py                  |  6 +++---
 uffd/invite/templates/invite/new.html | 10 +++++-----
 uffd/invite/views.py                  |  6 +++---
 3 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/tests/test_invite.py b/tests/test_invite.py
index dee382d0..0408ca6d 100644
--- a/tests/test_invite.py
+++ b/tests/test_invite.py
@@ -367,7 +367,7 @@ class TestInviteAdminViews(UffdTestCase):
 		r = self.client.get(path=url_for('invite.new'), follow_redirects=True)
 		dump('invite_new', r)
 		self.assertEqual(r.status_code, 200)
-		valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat()
+		valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes')
 		self.assertListEqual(Invite.query.all(), [])
 		r = self.client.post(path=url_for('invite.new_submit'),
 			data={'single-use': '1', 'valid-until': valid_until,
@@ -387,7 +387,7 @@ class TestInviteAdminViews(UffdTestCase):
 		db.session.add(role)
 		db.session.commit()
 		role_id = role.id
-		valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat()
+		valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes')
 		r = self.client.post(path=url_for('invite.new_submit'),
 			data={'single-use': '1', 'valid-until': valid_until,
 			      'allow-signup': '1', 'role-%d'%role_id: '1'}, follow_redirects=True)
@@ -397,7 +397,7 @@ class TestInviteAdminViews(UffdTestCase):
 	def test_new_empty(self):
 		current_app.config['ACL_SIGNUP_GROUP'] = 'uffd_access'
 		self.login_as('user')
-		valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat()
+		valid_until = (datetime.datetime.now() + datetime.timedelta(seconds=60)).isoformat(timespec='minutes')
 		r = self.client.post(path=url_for('invite.new_submit'),
 			data={'single-use': '1', 'valid-until': valid_until,
 			      'allow-signup': '0'}, follow_redirects=True)
diff --git a/uffd/invite/templates/invite/new.html b/uffd/invite/templates/invite/new.html
index 5c69f52e..0e734a75 100644
--- a/uffd/invite/templates/invite/new.html
+++ b/uffd/invite/templates/invite/new.html
@@ -5,21 +5,21 @@
 	<div class="form-group">
 		<label for="single-use">{{_('Link Type')}}</label>
 		<select class="form-control" id="single-use" name="single-use">
-			<option value="1">{{_('Valid for a single successful use')}}</option>
-			<option value="0">{{_('Multi-use')}}</option>
+			<option value="1" {{ 'selected' if request.values.get('single-use', '1') == '1' }}>{{_('Valid for a single successful use')}}</option>
+			<option value="0" {{ 'selected' if request.values.get('single-use', '1') == '0' }}>{{_('Multi-use')}}</option>
 		</select>
 	</div>
 	<div class="form-group">
 		<label for="valid-until">{{_('Valid Until')}}</label>
-		<input class="form-control" type="datetime-local" id="valid-until" name="valid-until" value="{{ (datetime.now() + timedelta(hours=36)).replace(hour=23, minute=59, second=59, microsecond=0).isoformat(timespec='minutes') }}">
+		<input class="form-control" type="datetime-local" id="valid-until" name="valid-until" value="{{ request.values.get('valid-until') or (datetime.now() + timedelta(hours=36)).replace(hour=23, minute=59).isoformat(timespec='minutes') }}" min="{{ datetime.now().isoformat(timespec='minutes') }}" max="{{ (datetime.now() + timedelta(days=config['INVITE_MAX_VALID_DAYS'])).isoformat(timespec='minutes') }}">
 		<small class="text-muted">{{_('Must be within the next %(max_valid_days)d days', max_valid_days=config['INVITE_MAX_VALID_DAYS'])}}</small>
 	</div>
 	{% if allow_signup %}
 	<div class="form-group">
 		<label for="allow-signup">{{_('Account Registration')}}</label>
 		<select class="form-control" id="allow-signup" name="allow-signup">
-			<option value="1">{{_('Link allows account registration')}}</option>
-			<option value="0">{{_('No account registration allowed')}}</option>
+			<option value="1" {{ 'selected' if request.values.get('allow-signup', '1') == '1' }}>{{_('Link allows account registration')}}</option>
+			<option value="0" {{ 'selected' if request.values.get('allow-signup', '1') == '0' }}>{{_('No account registration allowed')}}</option>
 		</select>
 	</div>
 	{% else %}
diff --git a/uffd/invite/views.py b/uffd/invite/views.py
index ef406294..820d4ff9 100644
--- a/uffd/invite/views.py
+++ b/uffd/invite/views.py
@@ -73,13 +73,13 @@ def new_submit():
 			invite.roles.append(Role.query.get(key[5:]))
 	if invite.valid_until > datetime.datetime.now() + datetime.timedelta(days=current_app.config['INVITE_MAX_VALID_DAYS']):
 		flash(_('The "Expires After" date is too far in the future'))
-		return redirect(url_for('invite.new'))
+		return new()
 	if not invite.permitted:
 		flash(_('You are not allowed to create invite links with these permissions'))
-		return redirect(url_for('invite.new'))
+		return new()
 	if not invite.allow_signup and not invite.roles:
 		flash(_('Invite link must either allow signup or grant at least one role'))
-		return redirect(url_for('invite.new'))
+		return new()
 	db.session.add(invite)
 	db.session.commit()
 	return redirect(url_for('invite.index'))
-- 
GitLab