Skip to content

Commit c3d4f1f

Browse files
vybhavgMariatta
andauthored
Fix/signup error style (#88)
Add django widget tweaks --------- Co-authored-by: Mariatta <[email protected]>
1 parent 513e4db commit c3d4f1f

File tree

6 files changed

+185
-43
lines changed

6 files changed

+185
-43
lines changed

.gitignore

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1+
# Python cache
2+
__pycache__/
3+
*.py[cod]
4+
5+
# Virtual envs
6+
.venv/
7+
venv/
8+
env/
9+
10+
# Env files
111
.envrc
212
.env.dev
313
.env.prod
414
.env.prod.db
515
.env
6-
.venv
7-
venv
8-
env
916

17+
# Coverage and state
1018
.coverage
1119
.state
12-
staticroot
1320

14-
__pycache__/
21+
# Django static build
22+
staticroot/
23+
24+
# Editor files
25+
.vscode/

portal/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"portal",
5050
"volunteer",
5151
"portal_account",
52+
"widget_tweaks",
5253
]
5354

5455
MIDDLEWARE = [

requirements-app.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ whitenoise==6.9.0
1010
dj-database-url==2.3.0
1111
boto3==1.38.5
1212
django-storages==1.14.6
13-
pillow==11.2.1
13+
django-widget-tweaks==1.5.0
14+
pillow==11.2.1

templates/account/signup.html

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,60 @@
11
{% extends "account/base_entrance.html" %}
22
{% load allauth i18n %}
33
{% load django_bootstrap5 %}
4+
{% load widget_tweaks %}
45
{% block head_title %}
56
{% trans "Signup" %}
67
{% endblock head_title %}
78
{% block content %}
89
{% element h1 %}
9-
{% trans "Sign Up" %}
10-
{% endelement %}
11-
{% setvar link %}
12-
<a href="{{ login_url }}">
13-
{% endsetvar %}
14-
{% setvar end_link %}
15-
</a>
16-
{% endsetvar %}
17-
{% element p %}
18-
{% blocktranslate %}Already have an account? Then please {{ link }}sign in{{ end_link }}.{% endblocktranslate %}
19-
{% endelement %}
20-
{% if not SOCIALACCOUNT_ONLY %}
21-
{% url 'account_signup' as action_url %}
22-
{% element form form=form method="post" action=action_url tags="entrance,signup" %}
23-
{% slot body %}
24-
{% csrf_token %}
25-
{% element fields form=form unlabeled=True %}
26-
{% endelement %}
27-
{{ redirect_field }}
28-
{% endslot %}
29-
{% slot actions %}
30-
{% element button tags="prominent,signup" type="submit" %}
31-
{% trans "Sign Up" %}
32-
{% endelement %}
33-
{% endslot %}
34-
{% endelement %}
35-
{% endif %}
36-
{% if PASSKEY_SIGNUP_ENABLED %}
37-
{% element hr %}
38-
{% endelement %}
39-
{% element button href=signup_by_passkey_url tags="prominent,signup,outline,primary" %}
40-
{% trans "Sign up using a passkey" %}
41-
{% endelement %}
42-
{% endif %}
43-
{% if SOCIALACCOUNT_ENABLED %}
44-
{% include "socialaccount/snippets/login.html" with page_layout="entrance" %}
45-
{% endif %}
10+
{% trans "Sign Up" %}
11+
{% endelement %}
12+
{% setvar link %}
13+
<a href="{{ login_url }}">
14+
{% endsetvar %}
15+
{% setvar end_link %}
16+
</a>
17+
{% endsetvar %}
18+
{% element p %}
19+
{% blocktranslate %}Already have an account? Then please {{ link }}sign in{{ end_link }}.{% endblocktranslate %}
20+
{% endelement %}
21+
{% if not SOCIALACCOUNT_ONLY %}
22+
{% url 'account_signup' as action_url %}
23+
24+
{% element form form=form method="post" action=action_url tags="entrance,signup" %}
25+
{% slot body %}
26+
{% csrf_token %}
27+
{% for field in form %}
28+
<div class="mb-3">
29+
{{ field.label_tag }}
30+
{% if field.errors %}
31+
{{ field|add_class:"form-control is-invalid" }}
32+
{% for error in field.errors %}
33+
<div class="invalid-feedback">{{ error }}</div>
34+
{% endfor %}
35+
{% else %}
36+
{{ field|add_class:"form-control" }}
37+
{% endif %}
38+
</div>
39+
{% endfor %}
40+
{{ redirect_field }}
41+
{% endslot %}
42+
{% slot actions %}
43+
{% element button tags="prominent,signup" type="submit" %}
44+
{% trans "Sign Up" %}
45+
{% endelement %}
46+
{% endslot %}
47+
{% endelement %}
48+
49+
{% endif %}
50+
{% if PASSKEY_SIGNUP_ENABLED %}
51+
{% element hr %}
52+
{% endelement %}
53+
{% element button href=signup_by_passkey_url tags="prominent,signup,outline,primary" %}
54+
{% trans "Sign up using a passkey" %}
55+
{% endelement %}
56+
{% endif %}
57+
{% if SOCIALACCOUNT_ENABLED %}
58+
{% include "socialaccount/snippets/login.html" with page_layout="entrance" %}
59+
{% endif %}
4660
{% endblock content %}

tests/portal/test_forms.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import pytest
2+
from allauth.account.forms import SignupForm
3+
from django.contrib.auth import get_user_model
4+
from django.contrib.sessions.middleware import SessionMiddleware
5+
from django.urls import reverse
6+
7+
from portal.forms import CustomSignupForm
8+
9+
User = get_user_model()
10+
11+
12+
@pytest.mark.django_db
13+
class TestCustomSignupForm:
14+
@pytest.fixture
15+
def form_data(self):
16+
return {
17+
"username": "testuser",
18+
"email": "[email protected]",
19+
"password1": "securepassword123",
20+
"password2": "securepassword123",
21+
"first_name": "Test",
22+
"last_name": "User",
23+
}
24+
25+
@pytest.fixture
26+
def request_with_session(self, client):
27+
"""Create a request with session using the Django test client"""
28+
# Make a dummy GET request
29+
response = client.get(reverse("account_signup"))
30+
request = response.wsgi_request
31+
32+
# Attach session middleware if not already
33+
middleware = SessionMiddleware(lambda r: None)
34+
middleware.process_request(request)
35+
request.session.save()
36+
37+
return request
38+
39+
def test_form_inherits_from_allauth(self):
40+
"""Ensure CustomSignupForm inherits from allauth's SignupForm"""
41+
assert issubclass(CustomSignupForm, SignupForm)
42+
43+
def test_form_has_custom_fields(self):
44+
"""Custom fields should be included and labeled correctly"""
45+
form = CustomSignupForm()
46+
assert "first_name" in form.fields
47+
assert "last_name" in form.fields
48+
assert form.fields["first_name"].label == "First Name"
49+
assert form.fields["last_name"].label == "Last Name"
50+
51+
def test_form_save_method(self, form_data, request_with_session):
52+
"""Check that the form saves user with correct fields"""
53+
form = CustomSignupForm(data=form_data)
54+
assert form.is_valid()
55+
56+
user = form.save(request_with_session)
57+
assert user.first_name == "Test"
58+
assert user.last_name == "User"
59+
assert user.username == "testuser"
60+
assert user.email == "[email protected]"
61+
62+
def test_form_validation_missing_names(self):
63+
"""Validation should fail if first/last name are missing"""
64+
data = {
65+
"username": "testuser",
66+
"email": "[email protected]",
67+
"password1": "securepassword123",
68+
"password2": "securepassword123",
69+
}
70+
form = CustomSignupForm(data=data)
71+
assert not form.is_valid()
72+
assert "first_name" in form.errors
73+
assert "last_name" in form.errors
74+
75+
def test_form_widget_attrs(self):
76+
"""Ensure widget attributes are set correctly"""
77+
form = CustomSignupForm()
78+
assert form.fields["first_name"].widget.attrs["placeholder"] == "First Name"
79+
assert form.fields["last_name"].widget.attrs["placeholder"] == "Last Name"

tests/portal_account/test_forms.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import pytest
2+
from django.urls import reverse
3+
from pytest_django.asserts import assertContains
24

35
from portal_account.forms import PortalProfileForm
46
from portal_account.models import PortalProfile
@@ -38,3 +40,37 @@ def test_profile_form_required_fields(self, portal_user):
3840
form_data = {"user": portal_user, "last_name": "new lname"}
3941
form = PortalProfileForm(user=portal_user, data=form_data)
4042
assert not form.is_valid()
43+
44+
45+
@pytest.mark.django_db
46+
class TestSignupView:
47+
def test_error_styling_on_invalid_signup(self, client):
48+
response = client.post(
49+
reverse("account_signup"),
50+
{
51+
"username": "", # Invalid empty username
52+
"email": "invalid-email", # Invalid email format
53+
"password1": "short", # Too short password
54+
"password2": "mismatch", # Password mismatch
55+
},
56+
)
57+
58+
# Verify error styling classes exist
59+
assertContains(
60+
response, "is-invalid", status_code=200
61+
) # Bootstrap invalid class
62+
assertContains(
63+
response, "invalid-feedback", status_code=200
64+
) # Error message class
65+
66+
# Verify specific field errors
67+
assertContains(response, "This field is required", status_code=200) # username
68+
assertContains(
69+
response, "Enter a valid email address", status_code=200
70+
) # email
71+
72+
def test_widget_tweaks_loaded(self, client):
73+
response = client.get(reverse("account_signup"))
74+
assertContains(
75+
response, "form-control", status_code=200
76+
) # Verify Bootstrap styling

0 commit comments

Comments
 (0)