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

inventree / InvenTree / 3753219419

pending completion
3753219419

push

github

GitHub
Replace include tag by the files contents (#4093)

24354 of 27795 relevant lines covered (87.62%)

0.88 hits per line

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

43.97
/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

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

29

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

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

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

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

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

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

54
        self.rebuild_layout()
×
55

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

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

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

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

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

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

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

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

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

111

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

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

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

124

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

128
    enter_password = forms.CharField(
1✔
129
        max_length=100,
130
        min_length=8,
131
        required=True,
132
        initial='',
133
        widget=forms.PasswordInput(attrs={'autocomplete': 'off'}),
134
        label=_('Enter password'),
135
        help_text=_('Enter new password')
136
    )
137

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

148
    old_password = forms.CharField(
1✔
149
        label=_("Old password"),
150
        strip=False,
151
        widget=forms.PasswordInput(attrs={'autocomplete': 'current-password', 'autofocus': True}),
152
    )
153

154
    class Meta:
1✔
155
        """Metaclass options."""
156

157
        model = User
1✔
158
        fields = [
1✔
159
            'enter_password',
160
            'confirm_password',
161
            'old_password',
162
        ]
163

164

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

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

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

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

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

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

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

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

205
        return cleaned_data
×
206

207

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

211
    def is_open_for_signup(self, request, *args, **kwargs):
1✔
212
        """Check if signup is enabled in settings."""
213
        if settings.EMAIL_HOST and InvenTreeSetting.get_setting('LOGIN_ENABLE_REG', True):
×
214
            return super().is_open_for_signup(request, *args, **kwargs)
×
215
        return False
×
216

217
    def save_user(self, request, user, form, commit=True):
1✔
218
        """Check if a default group is set in settings."""
219
        user = super().save_user(request, user, form)
×
220
        start_group = InvenTreeSetting.get_setting('SIGNUP_GROUP')
×
221
        if start_group:
×
222
            try:
×
223
                group = Group.objects.get(id=start_group)
×
224
                user.groups.add(group)
×
225
            except Group.DoesNotExist:
×
226
                logger.error('The setting `SIGNUP_GROUP` contains an non existant group', start_group)
×
227
        user.save()
×
228
        return user
×
229

230

231
class CustomUrlMixin:
1✔
232
    """Mixin to set urls."""
233

234
    def get_email_confirmation_url(self, request, emailconfirmation):
1✔
235
        """Custom email confirmation (activation) url."""
236
        url = reverse("account_confirm_email", args=[emailconfirmation.key])
×
237
        return Site.objects.get_current().domain + url
×
238

239

240
class CustomAccountAdapter(CustomUrlMixin, RegistratonMixin, OTPAdapter, DefaultAccountAdapter):
1✔
241
    """Override of adapter to use dynamic settings."""
242
    def send_mail(self, template_prefix, email, context):
1✔
243
        """Only send mail if backend configured."""
244
        if settings.EMAIL_HOST:
×
245
            return super().send_mail(template_prefix, email, context)
×
246
        return False
×
247

248

249
class CustomSocialAccountAdapter(CustomUrlMixin, RegistratonMixin, DefaultSocialAccountAdapter):
1✔
250
    """Override of adapter to use dynamic settings."""
251

252
    def is_auto_signup_allowed(self, request, sociallogin):
1✔
253
        """Check if auto signup is enabled in settings."""
254
        if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True):
×
255
            return super().is_auto_signup_allowed(request, sociallogin)
×
256
        return False
×
257

258
    # from OTPAdapter
259
    def has_2fa_enabled(self, user):
1✔
260
        """Returns True if the user has 2FA configured."""
261
        return user_has_valid_totp_device(user)
×
262

263
    def login(self, request, user):
1✔
264
        """Ensure user is send to 2FA before login if enabled."""
265
        # Require two-factor authentication if it has been configured.
266
        if self.has_2fa_enabled(user):
×
267
            # Cast to string for the case when this is not a JSON serializable
268
            # object, e.g. a UUID.
269
            request.session['allauth_2fa_user_id'] = str(user.id)
×
270

271
            redirect_url = reverse('two-factor-authenticate')
×
272
            # Add GET parameters to the URL if they exist.
273
            if request.GET:
×
274
                redirect_url += '?' + urlencode(request.GET)
×
275

276
            raise ImmediateHttpResponse(
×
277
                response=HttpResponseRedirect(redirect_url)
278
            )
279

280
        # Otherwise defer to the original allauth adapter.
281
        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

© 2026 Coveralls, Inc