• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wger-project / wger / 24964072477

26 Apr 2026 12:25PM UTC coverage: 86.106% (+0.4%) from 85.665%
24964072477

push

github

rolandgeider
Restrict trainer login access

17 of 18 new or added lines in 2 files covered. (94.44%)

91 existing lines in 9 files now uncovered.

17495 of 20318 relevant lines covered (86.11%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.91
/wger/core/forms.py
1
# -*- coding: utf-8 -*-
2

3
# This file is part of wger Workout Manager.
4
#
5
# wger Workout Manager is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Affero General Public License as published by
7
# the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# wger Workout Manager is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU Affero General Public License
16

17
# Standard Library
18
from datetime import date
1✔
19

20
# Django
21
from django import forms
1✔
22
from django.contrib.auth import authenticate
1✔
23
from django.contrib.auth.forms import (
1✔
24
    AuthenticationForm,
25
    PasswordResetForm,
26
    UserCreationForm,
27
)
28
from django.contrib.auth.models import User
1✔
29
from django.core.exceptions import ValidationError
1✔
30
from django.forms import (
1✔
31
    CharField,
32
    EmailField,
33
    Form,
34
    PasswordInput,
35
)
36
from django.utils.translation import (
1✔
37
    gettext as _,
38
    gettext_lazy,
39
)
40

41
# Third Party
42
from allauth.account.utils import filter_users_by_email
1✔
43
from crispy_forms.helper import FormHelper
1✔
44
from crispy_forms.layout import (
1✔
45
    HTML,
46
    ButtonHolder,
47
    Column,
48
    Fieldset,
49
    Layout,
50
    Row,
51
    Submit,
52
)
53
from django_recaptcha.fields import ReCaptchaField
1✔
54
from django_recaptcha.widgets import ReCaptchaV3
1✔
55

56
# wger
57
from wger.core.models import UserProfile
1✔
58

59

60
class PasswordInputWithToggle(PasswordInput):
1✔
61
    """
62
    Custom PasswordInput widget with eye icon toggle functionality
63
    """
64

65
    template_name = 'forms/password_with_toggle.html'
1✔
66

67
    def __init__(self, attrs=None, render_value=False):
1✔
68
        default_attrs = {'class': 'form-control'}
1✔
69
        if attrs:
1✔
UNCOV
70
            default_attrs.update(attrs)
×
71
        super().__init__(default_attrs, render_value)
1✔
72

73

74
class UserLoginForm(AuthenticationForm):
1✔
75
    """
76
    Form for logins
77
    """
78

79
    authenticate_on_clean = True
1✔
80

81
    def __init__(self, authenticate_on_clean=True, *args, **kwargs):
1✔
82
        super().__init__(*args, **kwargs)
1✔
83

84
        # Apply custom password widget
85
        self.fields['password'].widget = PasswordInputWithToggle()
1✔
86

87
        self.authenticate_on_clean = authenticate_on_clean
1✔
88

89
        self.helper = FormHelper()
1✔
90
        self.helper.add_input(Submit('submit', _('Login'), css_class='btn-success btn-block'))
1✔
91
        self.helper.form_class = 'wger-form'
1✔
92
        self.helper.layout = Layout(
1✔
93
            Row(
94
                Column('username', css_class='col-6'),
95
                Column('password', css_class='col-6'),
96
                css_class='form-row',
97
            )
98
        )
99

100
    def clean(self):
1✔
101
        """
102
        Note: this clean method needs to be able to toggle authenticating directly
103
        or not. This is needed because django axes expects an explicit request
104
        parameter and otherwise the login endpoint won't work
105

106
        See https://github.com/wger-project/wger/issues/1163
107
        """
108
        if self.authenticate_on_clean:
1✔
UNCOV
109
            self.authenticate(self.request)
×
110
        return self.cleaned_data
1✔
111

112
    def authenticate(self, request):
1✔
113
        username = self.cleaned_data.get('username')
1✔
114
        password = self.cleaned_data.get('password')
1✔
115

116
        if username and password:
1✔
117
            self.user_cache = authenticate(
1✔
118
                request=request,
119
                username=username,
120
                password=password,
121
            )
122
            if self.user_cache is None:
1✔
UNCOV
123
                raise self.get_invalid_login_error()
×
124
            else:
125
                self.confirm_login_allowed(self.user_cache)
1✔
126

127

128
class UserPreferencesForm(forms.ModelForm):
1✔
129
    first_name = forms.CharField(label=gettext_lazy('First name'), required=False)
1✔
130
    last_name = forms.CharField(label=gettext_lazy('Last name'), required=False)
1✔
131
    email = EmailField(
1✔
132
        label=gettext_lazy('Email'),
133
        help_text=gettext_lazy('Used for password resets and, optionally, e-mail reminders.'),
134
        required=False,
135
    )
136
    birthdate = forms.DateField(
1✔
137
        label=gettext_lazy('Date of Birth'),
138
        required=False,
139
        widget=forms.DateInput(
140
            attrs={
141
                'type': 'date',
142
                'max': str(date(date.today().year - 10, 1, 1)),
143
                'min': str(date(date.today().year - 100, 1, 1)),
144
            },
145
        ),
146
    )
147

148
    class Meta:
1✔
149
        model = UserProfile
1✔
150
        fields = (
1✔
151
            'show_comments',
152
            'show_english_ingredients',
153
            'workout_reminder_active',
154
            'workout_reminder',
155
            'workout_duration',
156
            'notification_language',
157
            'weight_unit',
158
            'ro_access',
159
            'num_days_weight_reminder',
160
            'birthdate',
161
            'height',
162
        )
163

164
    def __init__(self, *args, **kwargs):
1✔
165
        super(UserPreferencesForm, self).__init__(*args, **kwargs)
1✔
166

167
        hattrs = self.fields['height'].widget.attrs
1✔
168
        hattrs.setdefault('type', 'number')
1✔
169
        hattrs.setdefault('step', '1')
1✔
170
        hattrs['min'] = '0'
1✔
171

172
        self.helper = FormHelper()
1✔
173
        self.helper.form_class = 'wger-form'
1✔
174
        self.helper.layout = Layout(
1✔
175
            Fieldset(
176
                _('Personal data'),
177
                'email',
178
                Row(
179
                    Column('first_name', css_class='col-6'),
180
                    Column('last_name', css_class='col-6'),
181
                    css_class='form-row',
182
                ),
183
                'birthdate',
184
                'height',
185
                HTML('<hr>'),
186
            ),
187
            Fieldset(
188
                _('Workout reminders'),
189
                'workout_reminder_active',
190
                'workout_reminder',
191
                'workout_duration',
192
                HTML('<hr>'),
193
            ),
194
            Fieldset(
195
                _('Other settings'),
196
                'ro_access',
197
                'notification_language',
198
                'weight_unit',
199
                'show_comments',
200
                'show_english_ingredients',
201
                'num_days_weight_reminder',
202
            ),
203
            ButtonHolder(Submit('submit', _('Save'), css_class='btn-success btn-block')),
204
        )
205

206

207
class UserEmailForm(forms.ModelForm):
1✔
208
    email = EmailField(
1✔
209
        label=gettext_lazy('Email'),
210
        help_text=gettext_lazy('Used for password resets and, optionally, email reminders.'),
211
        required=False,
212
    )
213

214
    class Meta:
1✔
215
        model = User
1✔
216
        fields = ('email',)
1✔
217

218
    def clean_email(self):
1✔
219
        """
220
        E-mail must be unique system-wide.
221

222
        Uniqueness is checked case-insensitively across both ``User.email``
223
        and allauth's ``EmailAddress`` table (which also tracks secondary,
224
        unverified addresses)
225
        """
226

227
        email = self.cleaned_data['email']
1✔
228
        if not email:
1✔
229
            return email
1✔
230

231
        own_pk = self.instance.pk if self.instance and self.instance.pk else None
1✔
232
        if any(u.pk != own_pk for u in filter_users_by_email(email)):
1✔
233
            raise ValidationError(_('This e-mail address is already in use.'))
1✔
234
        return email
1✔
235

236

237
class UserPersonalInformationForm(UserEmailForm):
1✔
238
    first_name = forms.CharField(label=_('First name'), required=False)
1✔
239
    last_name = forms.CharField(label=_('Last name'), required=False)
1✔
240

241
    class Meta:
1✔
242
        model = User
1✔
243
        fields = ('first_name', 'last_name', 'email')
1✔
244

245

246
class PasswordConfirmationForm(Form):
1✔
247
    """
248
    A simple password confirmation form.
249

250
    This can be used to make sure the user really wants to perform a dangerous
251
    action. The form must be initialised with a user object.
252
    """
253

254
    password = CharField(
1✔
255
        label=gettext_lazy('Password'),
256
        widget=PasswordInputWithToggle,
257
        help_text=gettext_lazy('Please enter your current password.'),
258
    )
259

260
    def __init__(self, user, data=None):
1✔
261
        self.user = user
1✔
262
        super().__init__(data=data)
1✔
263
        self.helper = FormHelper()
1✔
264
        self.helper.layout = Layout(
1✔
265
            'password',
266
            ButtonHolder(Submit('submit', _('Delete'), css_class='btn-danger btn-block')),
267
        )
268

269
    def clean_password(self):
1✔
270
        """
271
        Check that the password supplied matches the one for the user
272
        """
273
        password = self.cleaned_data.get('password', None)
1✔
274
        if not self.user.check_password(password):
1✔
275
            raise ValidationError(_('Invalid password'))
1✔
276
        return self.cleaned_data.get('password')
1✔
277

278

279
class RegistrationForm(UserCreationForm, UserEmailForm):
1✔
280
    """
281
    Registration form with reCAPTCHA field
282
    """
283

284
    captcha = ReCaptchaField(
1✔
285
        widget=ReCaptchaV3(action='register'),
286
        label='reCaptcha',
287
        help_text=gettext_lazy('The form is secured with reCAPTCHA'),
288
    )
289

290
    def __init__(self, *args, **kwargs):
1✔
291
        super().__init__(*args, **kwargs)
1✔
292

293
        # Apply custom password widgets
294
        self.fields['password1'].widget = PasswordInputWithToggle()
1✔
295
        self.fields['password2'].widget = PasswordInputWithToggle()
1✔
296

297
        self.helper = FormHelper()
1✔
298
        self.helper.form_class = 'wger-form'
1✔
299
        self.helper.layout = Layout(
1✔
300
            'username',
301
            'email',
302
            Row(
303
                Column('password1', css_class='col-md-6 col-12'),
304
                Column('password2', css_class='col-md-6 col-12'),
305
                css_class='form-row',
306
            ),
307
            'captcha',
308
            ButtonHolder(Submit('submitBtn', _('Register'), css_class='btn-success btn-block')),
309
        )
310

311

312
class RegistrationFormNoCaptcha(UserCreationForm, UserEmailForm):
1✔
313
    """
314
    Registration form without CAPTCHA field
315
    """
316

317
    def __init__(self, *args, **kwargs):
1✔
318
        super().__init__(*args, **kwargs)
1✔
319

320
        # Apply custom password widgets
321
        self.fields['password1'].widget = PasswordInputWithToggle()
1✔
322
        self.fields['password2'].widget = PasswordInputWithToggle()
1✔
323

324
        self.helper = FormHelper()
1✔
325
        self.helper.form_class = 'wger-form'
1✔
326
        self.helper.layout = Layout(
1✔
327
            'username',
328
            'email',
329
            Row(
330
                Column('password1', css_class='col-md-6 col-12'),
331
                Column('password2', css_class='col-md-6 col-12'),
332
                css_class='form-row',
333
            ),
334
            ButtonHolder(
335
                Submit('submit', _('Register'), css_class='btn-success col-sm-6 col-12'),
336
                css_class='text-center',
337
            ),
338
        )
339

340

341
class PasswordResetFormCaptcha(PasswordResetForm):
1✔
342
    captcha = ReCaptchaField(
1✔
343
        widget=ReCaptchaV3(action='password_reset'),
344
        label='reCaptcha',
345
        help_text=gettext_lazy('The form is secured with reCAPTCHA'),
346
    )
347

348
    def __init__(self, *args, **kwargs):
1✔
UNCOV
349
        super().__init__(*args, **kwargs)
×
350

UNCOV
351
        self.helper = FormHelper()
×
UNCOV
352
        self.helper.form_class = 'wger-form'
×
353
        self.helper.layout = Layout(
×
354
            'email',
355
            'captcha',
356
            ButtonHolder(Submit('submitBtn', _('Submit'), css_class='btn-success btn-block')),
357
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc