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

rdmorganiser / rdmo / 14404330126

11 Apr 2025 01:31PM UTC coverage: 90.789% (+0.3%) from 90.478%
14404330126

push

github

web-flow
Merge pull request #1195 from rdmorganiser/2.3.0

RDMO 2.3.0 ⭐

989 of 1076 branches covered (91.91%)

9176 of 10107 relevant lines covered (90.79%)

3.63 hits per line

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

94.95
rdmo/projects/forms.py
1
from django import forms
4✔
2
from django.conf import settings
4✔
3
from django.contrib.auth import get_user_model
4✔
4
from django.contrib.sites.models import Site
4✔
5
from django.core.exceptions import ValidationError
4✔
6
from django.core.validators import EmailValidator
4✔
7
from django.db.models import Q
4✔
8
from django.utils.safestring import mark_safe
4✔
9
from django.utils.translation import gettext_lazy as _
4✔
10

11
from rdmo.core.constants import VALUE_TYPE_FILE
4✔
12
from rdmo.core.plugins import get_plugin
4✔
13
from rdmo.core.utils import markdown2html
4✔
14

15
from .constants import ROLE_CHOICES
4✔
16
from .models import Integration, IntegrationOption, Invite, Membership, Project, Snapshot, Visibility
4✔
17
from .validators import ProjectParentValidator
4✔
18

19

20
class CatalogChoiceField(forms.ModelChoiceField):
4✔
21

22
    _unavailable_icon = ' (<span class="fa fa-eye-slash" aria-hidden="true"></span>)'
4✔
23

24
    def label_from_instance(self, obj):
4✔
25
        rendered_title = markdown2html(obj.title)
4✔
26
        rendered_help = markdown2html(obj.help)
4✔
27

28
        if obj.available is False:
4✔
29
            return mark_safe(f'<div class="text-muted"><p>{rendered_title}{self._unavailable_icon}</p>'
4✔
30
                             f'<p>{rendered_help}</p></div>')
31

32
        return mark_safe(f'<p><b>{rendered_title}</b></p><p>{rendered_help}</p>')
4✔
33

34

35
class TasksMultipleChoiceField(forms.ModelMultipleChoiceField):
4✔
36

37
    def label_from_instance(self, obj):
4✔
38
        rendered_title = markdown2html(obj.title)
4✔
39
        rendered_text = markdown2html(obj.text)
4✔
40

41
        return mark_safe(f'<b>{rendered_title}</b></br>{rendered_text}')
4✔
42

43

44
class ViewsMultipleChoiceField(forms.ModelMultipleChoiceField):
4✔
45

46
    def label_from_instance(self, obj):
4✔
47
        rendered_title = markdown2html(obj.title)
4✔
48
        rendered_help = markdown2html(obj.help)
4✔
49

50
        return mark_safe(f'<b>{rendered_title}</b></br>{rendered_help}')
4✔
51

52

53
class ProjectForm(forms.ModelForm):
4✔
54

55
    use_required_attribute = False
4✔
56

57
    def __init__(self, *args, **kwargs):
4✔
58
        self.copy = kwargs.pop('copy', False)
4✔
59

60
        catalogs = kwargs.pop('catalogs')
4✔
61
        projects = kwargs.pop('projects')
4✔
62
        super().__init__(*args, **kwargs)
4✔
63
        self.fields['title'].widget.attrs.update({
4✔
64
            'autofocus': True
65
        })
66
        self.fields['catalog'].queryset = catalogs
4✔
67
        self.fields['catalog'].empty_label = None
4✔
68
        self.fields['catalog'].initial = catalogs.first()
4✔
69

70
        if settings.NESTED_PROJECTS:
4✔
71
            self.fields['parent'].queryset = projects
4✔
72

73
    def clean(self):
4✔
74
        if not self.copy:
4✔
75
            ProjectParentValidator(self.instance)(self.cleaned_data)
4✔
76
        super().clean()
4✔
77

78
    class Meta:
4✔
79
        model = Project
4✔
80

81
        fields = ['title', 'description', 'catalog']
4✔
82
        if settings.NESTED_PROJECTS:
4✔
83
            fields += ['parent']
4✔
84

85
        field_classes = {
4✔
86
            'catalog': CatalogChoiceField
87
        }
88
        widgets = {
4✔
89
            'catalog': forms.RadioSelect()
90
        }
91

92

93
class ProjectUpdateInformationForm(forms.ModelForm):
4✔
94

95
    use_required_attribute = False
4✔
96

97
    class Meta:
4✔
98
        model = Project
4✔
99
        fields = ('title', 'description')
4✔
100

101

102
class ProjectUpdateVisibilityForm(forms.ModelForm):
4✔
103

104
    use_required_attribute = False
4✔
105

106
    def __init__(self, *args, **kwargs):
4✔
107
        self.project = kwargs.pop('instance')
4✔
108
        self.user = kwargs.pop('user')
4✔
109
        self.site = kwargs.pop('site')
4✔
110

111
        try:
4✔
112
            instance = self.project.visibility
4✔
113
        except Visibility.DoesNotExist:
4✔
114
            instance = None
4✔
115

116
        super().__init__(*args, instance=instance, **kwargs)
4✔
117

118
        # remove the sites or group sets if they are not needed, doing this in Meta would break tests
119
        if not (settings.MULTISITE and self.user.has_perm('projects.change_visibility')):
4✔
120
            self.fields.pop('sites')
4✔
121
        if not settings.GROUPS:
4✔
122
            self.fields.pop('groups')
4✔
123

124
    class Meta:
4✔
125
        model = Visibility
4✔
126
        fields = ('sites', 'groups')
4✔
127

128
    def save(self, *args, **kwargs):
4✔
129
        if 'cancel' in self.data:
4✔
130
            pass
×
131
        elif 'delete' in self.data:
4✔
132
            if settings.MULTISITE and not self.user.has_perm('projects.delete_visibility'):
4✔
133
                current_site = Site.objects.get(id=self.site.id)
4✔
134
                self.instance.remove_site(current_site)
4✔
135
            else:
136
                self.instance.delete()
4✔
137
        else:
138
            visibility, created = Visibility.objects.update_or_create(project=self.project)
4✔
139

140
            if settings.MULTISITE:
4✔
141
                if self.user.has_perm('projects.change_visibility'):
4✔
142
                    visibility.sites.set(self.cleaned_data.get('sites'))
4✔
143
                else:
144
                    visibility.sites.add(self.site)
4✔
145

146
            if settings.GROUPS:
4✔
147
                visibility.groups.set(self.cleaned_data.get('groups'))
4✔
148

149
        return self.project
4✔
150

151

152
class ProjectUpdateCatalogForm(forms.ModelForm):
4✔
153

154
    use_required_attribute = False
4✔
155

156
    def __init__(self, *args, **kwargs):
4✔
157
        catalogs = kwargs.pop('catalogs')
4✔
158
        super().__init__(*args, **kwargs)
4✔
159
        self.fields['catalog'].queryset = catalogs
4✔
160
        self.fields['catalog'].empty_label = None
4✔
161

162
    class Meta:
4✔
163
        model = Project
4✔
164
        fields = ('catalog', )
4✔
165
        field_classes = {
4✔
166
            'catalog': CatalogChoiceField
167
        }
168
        widgets = {
4✔
169
            'catalog': forms.RadioSelect()
170
        }
171

172
    def save(self, *args, **kwargs):
4✔
173
        # if the catalog is the same, do nothing
174
        if self.instance.catalog.id == self.cleaned_data.get('catalog'):
4✔
175
            return self.instance
×
176
        return super().save(*args, **kwargs)
4✔
177

178

179
class ProjectUpdateTasksForm(forms.ModelForm):
4✔
180

181
    use_required_attribute = False
4✔
182

183
    def __init__(self, *args, **kwargs):
4✔
184
        if settings.PROJECT_TASKS_SYNC:
4✔
185
            raise ValidationError(_("Editing tasks is disabled."))
×
186

187
        tasks = kwargs.pop('tasks')
4✔
188
        super().__init__(*args, **kwargs)
4✔
189
        self.fields['tasks'].queryset = tasks
4✔
190

191
    class Meta:
4✔
192
        model = Project
4✔
193
        fields = ('tasks', )
4✔
194
        field_classes = {
4✔
195
            'tasks': TasksMultipleChoiceField
196
        }
197
        widgets = {
4✔
198
            'tasks': forms.CheckboxSelectMultiple()
199
        }
200

201
    def save(self, *args, **kwargs):
4✔
202
        if settings.PROJECT_TASKS_SYNC:
4✔
203
            raise ValidationError(_("Editing tasks is disabled."))
×
204
        super().save(*args, **kwargs)
4✔
205

206

207
class ProjectUpdateViewsForm(forms.ModelForm):
4✔
208

209
    use_required_attribute = False
4✔
210

211
    def __init__(self, *args, **kwargs):
4✔
212
        if settings.PROJECT_VIEWS_SYNC:
4✔
213
            raise ValidationError(_("Editing views is disabled."))
×
214

215
        views = kwargs.pop('views')
4✔
216
        super().__init__(*args, **kwargs)
4✔
217
        self.fields['views'].queryset = views
4✔
218

219
    class Meta:
4✔
220
        model = Project
4✔
221
        fields = ('views', )
4✔
222
        field_classes = {
4✔
223
            'views': ViewsMultipleChoiceField
224
        }
225
        widgets = {
4✔
226
            'views': forms.CheckboxSelectMultiple()
227
        }
228

229
    def save(self, *args, **kwargs):
4✔
230
        if settings.PROJECT_VIEWS_SYNC:
4✔
231
            raise ValidationError(_("Editing views is disabled."))
×
232
        super().save(*args, **kwargs)
4✔
233

234

235
class ProjectUpdateParentForm(forms.ModelForm):
4✔
236

237
    use_required_attribute = False
4✔
238

239
    def __init__(self, *args, **kwargs):
4✔
240
        projects = kwargs.pop('projects')
4✔
241
        super().__init__(*args, **kwargs)
4✔
242
        self.fields['parent'].queryset = projects
4✔
243

244
    def clean(self):
4✔
245
        ProjectParentValidator(self.instance)(self.cleaned_data)
4✔
246
        super().clean()
4✔
247

248
    class Meta:
4✔
249
        model = Project
4✔
250
        fields = ('parent', )
4✔
251

252

253
class SnapshotCreateForm(forms.ModelForm):
4✔
254

255
    use_required_attribute = False
4✔
256

257
    class Meta:
4✔
258
        model = Snapshot
4✔
259
        fields = ('title', 'description')
4✔
260

261
    def __init__(self, *args, **kwargs):
4✔
262
        self.project = kwargs.pop('project')
4✔
263
        super().__init__(*args, **kwargs)
4✔
264

265
    def save(self, *args, **kwargs):
4✔
266
        self.instance.project = self.project
4✔
267
        return super().save(*args, **kwargs)
4✔
268

269

270
class MembershipCreateForm(forms.Form):
4✔
271

272
    use_required_attribute = False
4✔
273

274
    username_or_email = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('Username or e-mail')}),
4✔
275
                                        label=_('User'),
276
                                        help_text=_('The username or e-mail of the new user.'))
277
    role = forms.CharField(widget=forms.RadioSelect(choices=ROLE_CHOICES),
4✔
278
                           initial='author')
279

280
    def __init__(self, *args, **kwargs):
4✔
281
        self.project = kwargs.pop('project')
4✔
282
        self.is_site_manager = kwargs.pop('is_site_manager')
4✔
283
        super().__init__(*args, **kwargs)
4✔
284

285
        if self.is_site_manager:
4✔
286
            self.fields['silent'] = forms.BooleanField(
4✔
287
                required=False,
288
                label=_('Add member silently'),
289
                help_text=_('As site manager or admin, you can directly add users without notifying them via e-mail, '
290
                            'when you check the following checkbox.')
291
            )
292

293
    def clean_username_or_email(self):
4✔
294
        username_or_email = self.cleaned_data['username_or_email']
4✔
295
        usermodel = get_user_model()
4✔
296

297
        # check if it is a registered user
298
        try:
4✔
299
            self.cleaned_data['user'] = usermodel.objects.get(Q(username=username_or_email) |
4✔
300
                                                              Q(email__iexact=username_or_email))
301
            self.cleaned_data['email'] = self.cleaned_data['user'].email
4✔
302

303
            if self.cleaned_data['user'] in self.project.user.all():
4✔
304
                raise ValidationError(_('The user is already a member of the project.'))
4✔
305

306
        except (usermodel.DoesNotExist, usermodel.MultipleObjectsReturned) as e:
4✔
307
            if settings.PROJECT_SEND_INVITE:
4✔
308
                # check if it is a valid email address, this will raise the correct ValidationError
309
                EmailValidator()(username_or_email)
4✔
310

311
                self.cleaned_data['user'] = None
4✔
312
                self.cleaned_data['email'] = username_or_email
4✔
313
            else:
314
                self.cleaned_data['user'] = None
4✔
315
                self.cleaned_data['email'] = None
4✔
316
                raise ValidationError(_('A user with this username or e-mail was not found. '
4✔
317
                                        'Only registered users can be invited.')) from e
318

319
    def clean(self):
4✔
320
        if self.cleaned_data.get('silent') is True and self.cleaned_data.get('user') is None:
4✔
321
            raise ValidationError(_('Only existing users can be added silently.'))
×
322

323
    def save(self):
4✔
324
        if self.is_site_manager and self.cleaned_data.get('silent') is True:
4✔
325
            Membership.objects.create(
4✔
326
                project=self.project,
327
                user=self.cleaned_data.get('user'),
328
                role=self.cleaned_data.get('role')
329
            )
330
        else:
331
            invite, created = Invite.objects.get_or_create(
4✔
332
                project=self.project,
333
                user=self.cleaned_data.get('user'),
334
                email=self.cleaned_data.get('email')
335
            )
336
            invite.role = self.cleaned_data.get('role')
4✔
337
            invite.make_token()
4✔
338
            invite.save()
4✔
339

340
            return invite
4✔
341

342

343
class IntegrationForm(forms.ModelForm):
4✔
344

345
    class Meta:
4✔
346
        model = Integration
4✔
347
        fields = ()
4✔
348

349
    def __init__(self, *args, **kwargs):
4✔
350
        self.project = kwargs.pop('project')
4✔
351
        self.provider_key = kwargs.pop('provider_key', None)
4✔
352
        super().__init__(*args, **kwargs)
4✔
353

354
        # get the provider
355
        if self.provider_key:
4✔
356
            self.provider = get_plugin('PROJECT_ISSUE_PROVIDERS', self.provider_key)
4✔
357
        else:
358
            self.provider = self.instance.provider
4✔
359

360
        # add fields for the integration options
361
        for field in self.provider.fields:
4✔
362
            # new integration instance is going to be created
363
            if self.instance.pk is None:
4✔
364
                initial = None
4✔
365
            # existing integration is going to be updated
366
            else:
367
                try:
4✔
368
                    initial = IntegrationOption.objects.get(integration=self.instance, key=field.get('key')).value
4✔
369
                except IntegrationOption.DoesNotExist:
×
370
                    initial = None
×
371

372
            if field.get('placeholder'):
4✔
373
                attrs = {'placeholder': field.get('placeholder')}
4✔
374

375
            self.fields[field.get('key')] = forms.CharField(widget=forms.TextInput(attrs=attrs),
4✔
376
                                                            initial=initial, required=field.get('required', True),
377
                                                            help_text=field.get('help'))
378

379
    def save(self):
4✔
380
        # the the project and the provider_key
381
        self.instance.project = self.project
4✔
382
        if self.provider_key:
4✔
383
            self.instance.provider_key = self.provider_key
4✔
384

385
        # call the form's save method
386
        super().save()
4✔
387

388
        # save the integration options
389
        self.instance.save_options(self.cleaned_data)
4✔
390

391

392
class IssueSendForm(forms.Form):
4✔
393

394
    class AttachmentViewsField(forms.ModelMultipleChoiceField):
4✔
395
        def label_from_instance(self, obj):
4✔
396
            return _('Attach %s') % obj.title
4✔
397

398
    class AttachmentFilesField(forms.ModelMultipleChoiceField):
4✔
399
        def label_from_instance(self, obj):
4✔
400
            return _('Attach %s') % obj.file_name
×
401

402
    class AttachmentSnapshotField(forms.ModelChoiceField):
4✔
403
        def label_from_instance(self, obj):
4✔
404
            return obj.title
4✔
405

406
    subject = forms.CharField(label=_('Subject'), max_length=128)
4✔
407
    message = forms.CharField(label=_('Message'), widget=forms.Textarea)
4✔
408

409
    def __init__(self, *args, **kwargs):
4✔
410
        self.project = kwargs.pop('project')
4✔
411
        super().__init__(*args, **kwargs)
4✔
412

413
        self.fields['attachments_answers'] = forms.MultipleChoiceField(
4✔
414
            label=_('Answers'), widget=forms.CheckboxSelectMultiple, required=False,
415
            choices=[('project_answers', _('Attach the output of "View answers".'))]
416
        )
417
        self.fields['attachments_views'] = self.AttachmentViewsField(
4✔
418
            label=_('Views'), widget=forms.CheckboxSelectMultiple, required=False,
419
            queryset=self.project.views.all(), to_field_name='id'
420
        )
421
        self.fields['attachments_files'] = self.AttachmentFilesField(
4✔
422
            label=_('Files'), widget=forms.CheckboxSelectMultiple, required=False,
423
            queryset=self.project.values.filter(snapshot=None)
424
                                        .filter(value_type=VALUE_TYPE_FILE)
425
                                        .order_by('file'),
426
            to_field_name='id'
427
        )
428
        self.fields['attachments_snapshot'] = self.AttachmentSnapshotField(
4✔
429
            label=_('Snapshot'), widget=forms.RadioSelect, required=False,
430
            queryset=self.project.snapshots.all(), empty_label=_('Current')
431
        )
432
        self.fields['attachments_format'] = forms.ChoiceField(
4✔
433
            label=_('Format'), widget=forms.RadioSelect, required=False,
434
            choices=settings.EXPORT_FORMATS
435
        )
436

437
    def clean(self):
4✔
438
        cleaned_data = super().clean()
4✔
439

440
        if cleaned_data.get('attachments_answers') or cleaned_data.get('attachments_views'):
4✔
441
            if not cleaned_data.get('attachments_format'):
4✔
442
                self.add_error('attachments_format', _('This field is required.'))
×
443

444

445
class IssueMailForm(forms.Form):
4✔
446

447
    if settings.EMAIL_RECIPIENTS_CHOICES:
4✔
448
        recipients = forms.MultipleChoiceField(label=_('Recipients'), widget=forms.CheckboxSelectMultiple,
4✔
449
                                               required=not settings.EMAIL_RECIPIENTS_INPUT,
450
                                               choices=settings.EMAIL_RECIPIENTS_CHOICES)
451

452
    if settings.EMAIL_RECIPIENTS_INPUT:
4✔
453
        recipients_input = forms.CharField(label=_('Recipients'), widget=forms.Textarea(attrs={
4✔
454
            'placeholder': _('Enter recipients line by line')
455
        }), required=not settings.EMAIL_RECIPIENTS_CHOICES)
456

457
    def clean(self):
4✔
458
        cleaned_data = super().clean()
4✔
459

460
        if settings.EMAIL_RECIPIENTS_INPUT and \
4✔
461
                cleaned_data.get('recipients') == [] and \
462
                cleaned_data.get('recipients_input') == []:
463
            self.add_error('recipients_input', _('This field is required.'))
4✔
464

465
    def clean_recipients_input(self):
4✔
466
        email_validator = EmailValidator()
4✔
467
        cleaned_data = []
4✔
468

469
        for line in self.cleaned_data['recipients_input'].splitlines():
4✔
470
            email = line.strip()
×
471
            email_validator(email)
×
472
            cleaned_data.append(email)
×
473

474
        return cleaned_data
4✔
475

476

477
class UploadFileForm(forms.Form):
4✔
478
    uploaded_file = forms.FileField(
4✔
479
        label='Select a file',
480
    )
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