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

inventree / InvenTree / 4509843514

pending completion
4509843514

push

github

Oliver Walters
Add result counter to quick search bar

25881 of 29379 relevant lines covered (88.09%)

0.88 hits per line

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

38.13
/InvenTree/InvenTree/forms.py
1
"""Helper forms which subclass Django forms to provide additional functionality."""
2

3
import logging
1✔
4
from urllib.parse import urlencode
1✔
5

6
from django import forms
1✔
7
from django.conf import settings
1✔
8
from django.contrib.auth.models import Group, User
1✔
9
from django.contrib.sites.models import Site
1✔
10
from django.http import HttpResponseRedirect
1✔
11
from django.urls import reverse
1✔
12
from django.utils.translation import gettext_lazy as _
1✔
13

14
from allauth.account.adapter import DefaultAccountAdapter
1✔
15
from allauth.account.forms import SignupForm, set_form_field_order
1✔
16
from allauth.exceptions import ImmediateHttpResponse
1✔
17
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
1✔
18
from allauth_2fa.adapter import OTPAdapter
1✔
19
from allauth_2fa.utils import user_has_valid_totp_device
1✔
20
from crispy_forms.bootstrap import (AppendedText, PrependedAppendedText,
1✔
21
                                    PrependedText)
22
from crispy_forms.helper import FormHelper
1✔
23
from crispy_forms.layout import Field, Layout
1✔
24

25
from common.models import InvenTreeSetting
1✔
26
from InvenTree.exceptions import log_error
1✔
27

28
logger = logging.getLogger('inventree')
1✔
29

30

31
class HelperForm(forms.ModelForm):
1✔
32
    """Provides simple integration of crispy_forms extension."""
33

34
    # Custom field decorations can be specified here, per form class
35
    field_prefix = {}
1✔
36
    field_suffix = {}
1✔
37
    field_placeholder = {}
1✔
38

39
    def __init__(self, *args, **kwargs):
1✔
40
        """Setup layout."""
41
        super(forms.ModelForm, self).__init__(*args, **kwargs)
×
42
        self.helper = FormHelper()
×
43

44
        self.helper.form_tag = False
×
45
        self.helper.form_show_errors = True
×
46

47
        """
48
        Create a default 'layout' for this form.
49
        Ref: https://django-crispy-forms.readthedocs.io/en/latest/layouts.html
50
        This is required to do fancy things later (like adding PrependedText, etc).
51

52
        Simply create a 'blank' layout for each available field.
53
        """
54

55
        self.rebuild_layout()
×
56

57
    def rebuild_layout(self):
1✔
58
        """Build crispy layout out of current fields."""
59
        layouts = []
×
60

61
        for field in self.fields:
×
62
            prefix = self.field_prefix.get(field, None)
×
63
            suffix = self.field_suffix.get(field, None)
×
64
            placeholder = self.field_placeholder.get(field, '')
×
65

66
            # Look for font-awesome icons
67
            if prefix and prefix.startswith('fa-'):
×
68
                prefix = r"<i class='fas {fa}'/>".format(fa=prefix)
×
69

70
            if suffix and suffix.startswith('fa-'):
×
71
                suffix = r"<i class='fas {fa}'/>".format(fa=suffix)
×
72

73
            if prefix and suffix:
×
74
                layouts.append(
×
75
                    Field(
76
                        PrependedAppendedText(
77
                            field,
78
                            prepended_text=prefix,
79
                            appended_text=suffix,
80
                            placeholder=placeholder
81
                        )
82
                    )
83
                )
84

85
            elif prefix:
×
86
                layouts.append(
×
87
                    Field(
88
                        PrependedText(
89
                            field,
90
                            prefix,
91
                            placeholder=placeholder
92
                        )
93
                    )
94
                )
95

96
            elif suffix:
×
97
                layouts.append(
×
98
                    Field(
99
                        AppendedText(
100
                            field,
101
                            suffix,
102
                            placeholder=placeholder
103
                        )
104
                    )
105
                )
106

107
            else:
108
                layouts.append(Field(field, placeholder=placeholder))
×
109

110
        self.helper.layout = Layout(*layouts)
×
111

112

113
class EditUserForm(HelperForm):
1✔
114
    """Form for editing user information."""
115

116
    class Meta:
1✔
117
        """Metaclass options."""
118

119
        model = User
1✔
120
        fields = [
1✔
121
            'first_name',
122
            'last_name',
123
        ]
124

125

126
class SetPasswordForm(HelperForm):
1✔
127
    """Form for setting user password."""
128

129
    class Meta:
1✔
130
        """Metaclass options."""
131

132
        model = User
1✔
133
        fields = [
1✔
134
            'enter_password',
135
            'confirm_password',
136
            'old_password',
137
        ]
138

139
    enter_password = forms.CharField(
1✔
140
        max_length=100,
141
        min_length=8,
142
        required=True,
143
        initial='',
144
        widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
145
        label=_('Enter password'),
146
        help_text=_('Enter new password')
147
    )
148

149
    confirm_password = forms.CharField(
1✔
150
        max_length=100,
151
        min_length=8,
152
        required=True,
153
        initial='',
154
        widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
155
        label=_('Confirm password'),
156
        help_text=_('Confirm new password')
157
    )
158

159
    old_password = forms.CharField(
1✔
160
        label=_("Old password"),
161
        strip=False,
162
        widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}),
163
    )
164

165

166
# override allauth
167
class CustomSignupForm(SignupForm):
1✔
168
    """Override to use dynamic settings."""
169

170
    def __init__(self, *args, **kwargs):
1✔
171
        """Check settings to influence which fields are needed."""
172
        kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED')
×
173

174
        super().__init__(*args, **kwargs)
×
175

176
        # check for two mail fields
177
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
×
178
            self.fields["email2"] = forms.EmailField(
×
179
                label=_("Email (again)"),
180
                widget=forms.TextInput(
181
                    attrs={
182
                        "type": "email",
183
                        "placeholder": _("Email address confirmation"),
184
                    }
185
                ),
186
            )
187

188
        # check for two password fields
189
        if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'):
×
190
            self.fields.pop("password2")
×
191

192
        # reorder fields
193
        set_form_field_order(self, ["username", "email", "email2", "password1", "password2", ])
×
194

195
    def clean(self):
1✔
196
        """Make sure the supllied emails match if enabled in settings."""
197
        cleaned_data = super().clean()
×
198

199
        # check for two mail fields
200
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
×
201
            email = cleaned_data.get("email")
×
202
            email2 = cleaned_data.get("email2")
×
203
            if (email and email2) and email != email2:
×
204
                self.add_error("email2", _("You must type the same email each time."))
×
205

206
        return cleaned_data
×
207

208

209
class RegistratonMixin:
1✔
210
    """Mixin to check if registration should be enabled."""
211

212
    def is_open_for_signup(self, request, *args, **kwargs):
1✔
213
        """Check if signup is enabled in settings.
214

215
        Configure the class variable `REGISTRATION_SETTING` to set which setting should be used, defualt: `LOGIN_ENABLE_REG`.
216
        """
217
        if settings.EMAIL_HOST and (InvenTreeSetting.get_setting('LOGIN_ENABLE_REG') or InvenTreeSetting.get_setting('LOGIN_ENABLE_SSO_REG')):
×
218
            return super().is_open_for_signup(request, *args, **kwargs)
×
219
        return False
×
220

221
    def clean_email(self, email):
1✔
222
        """Check if the mail is valid to the pattern in LOGIN_SIGNUP_MAIL_RESTRICTION (if enabled in settings)."""
223
        mail_restriction = InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_RESTRICTION', None)
×
224
        if not mail_restriction:
×
225
            return super().clean_email(email)
×
226

227
        split_email = email.split('@')
×
228
        if len(split_email) != 2:
×
229
            logger.error(f'The user {email} has an invalid email address')
×
230
            raise forms.ValidationError(_('The provided primary email address is not valid.'))
×
231

232
        mailoptions = mail_restriction.split(',')
×
233
        for option in mailoptions:
×
234
            if not option.startswith('@'):
×
235
                log_error('LOGIN_SIGNUP_MAIL_RESTRICTION is not configured correctly')
×
236
                raise forms.ValidationError(_('The provided primary email address is not valid.'))
×
237
            else:
238
                if split_email[1] == option[1:]:
×
239
                    return super().clean_email(email)
×
240

241
        logger.info(f'The provided email domain for {email} is not approved')
×
242
        raise forms.ValidationError(_('The provided email domain is not approved.'))
×
243

244
    def save_user(self, request, user, form, commit=True):
1✔
245
        """Check if a default group is set in settings."""
246
        # Create the user
247
        user = super().save_user(request, user, form)
×
248

249
        # Check if a default group is set in settings
250
        start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP')
×
251
        if start_group:
×
252
            try:
×
253
                group = Group.objects.get(id=start_group)
×
254
                user.groups.add(group)
×
255
            except Group.DoesNotExist:
×
256
                logger.error('The setting `SIGNUP_GROUP` contains an non existant group', start_group)
×
257
        user.save()
×
258
        return user
×
259

260

261
class CustomUrlMixin:
1✔
262
    """Mixin to set urls."""
263

264
    def get_email_confirmation_url(self, request, emailconfirmation):
1✔
265
        """Custom email confirmation (activation) url."""
266
        url = reverse("account_confirm_email", args=[emailconfirmation.key])
×
267
        return Site.objects.get_current().domain + url
×
268

269

270
class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
1✔
271
    """Override of adapter to use dynamic settings."""
272

273
    def send_mail(self, template_prefix, email, context):
1✔
274
        """Only send mail if backend configured."""
275
        if settings.EMAIL_HOST:
×
276
            try:
×
277
                result = super().send_mail(template_prefix, email, context)
×
278
            except Exception:
×
279
                # An exception ocurred while attempting to send email
280
                # Log it (for admin users) and return silently
281
                log_error('account email')
×
282
                result = False
×
283

284
            return result
×
285

286
        return False
×
287

288

289
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
1✔
290
    """Override of adapter to use dynamic settings."""
291

292
    def is_auto_signup_allowed(self, request, sociallogin):
1✔
293
        """Check if auto signup is enabled in settings."""
294
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True):
×
295
            return super().is_auto_signup_allowed(request, sociallogin)
×
296
        return False
×
297

298
    # from OTPAdapter
299
    def has_2fa_enabled(self, user):
1✔
300
        """Returns True if the user has 2FA configured."""
301
        return user_has_valid_totp_device(user)
×
302

303
    def login(self, request, user):
1✔
304
        """Ensure user is send to 2FA before login if enabled."""
305
        # Require two-factor authentication if it has been configured.
306
        if self.has_2fa_enabled(user):
×
307
            # Cast to string for the case when this is not a JSON serializable
308
            # object, e.g. a UUID.
309
            request.session['allauth_2fa_user_id'] = str(user.id)
×
310

311
            redirect_url = reverse('two-factor-authenticate')
×
312
            # Add GET parameters to the URL if they exist.
313
            if request.GET:
×
314
                redirect_url += '?' + urlencode(request.GET)
×
315

316
            raise ImmediateHttpResponse(
×
317
                response=HttpResponseRedirect(redirect_url)
318
            )
319

320
        # Otherwise defer to the original allauth adapter.
321
        return super().login(request, user)
×
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

© 2025 Coveralls, Inc