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

rdmorganiser / rdmo / 13202710339

07 Feb 2025 03:18PM UTC coverage: 90.534% (+0.06%) from 90.478%
13202710339

Pull #1195

github

web-flow
Merge 6d6279dd4 into 1eadee519
Pull Request #1195: RDMO 2.3.0 ⭐

1466 of 1619 branches covered (90.55%)

8780 of 9698 relevant lines covered (90.53%)

3.62 hits per line

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

96.08
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.core.exceptions import ValidationError
4✔
5
from django.core.validators import EmailValidator
4✔
6
from django.db.models import Q
4✔
7
from django.utils.safestring import mark_safe
4✔
8
from django.utils.translation import gettext_lazy as _
4✔
9

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

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

18

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

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

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

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

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

33

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

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

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

42

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

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

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

51

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

54
    use_required_attribute = False
4✔
55

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

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

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

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

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

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

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

91

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

94
    use_required_attribute = False
4✔
95

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

100

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

103
    use_required_attribute = False
4✔
104

105
    def __init__(self, *args, **kwargs):
4✔
106
        self.project = kwargs.pop('instance')
4✔
107
        try:
4✔
108
            instance = self.project.visibility
4✔
109
        except Visibility.DoesNotExist:
4✔
110
            instance = None
4✔
111

112
        super().__init__(*args, instance=instance, **kwargs)
4✔
113

114
        # remove the sites or group sets if they are not needed, doing this in Meta would break tests
115
        if not settings.MULTISITE:
4✔
116
            self.fields.pop('sites')
4✔
117
        if not settings.GROUPS:
4✔
118
            self.fields.pop('groups')
4✔
119

120
    class Meta:
4✔
121
        model = Visibility
4✔
122
        fields = ('sites', 'groups')
4✔
123

124
    def save(self, *args, **kwargs):
4✔
125
        if 'cancel' in self.data:
4✔
126
            pass
×
127
        elif 'delete' in self.data:
4✔
128
            self.instance.delete()
4✔
129
        else:
130
            visibility, created = Visibility.objects.update_or_create(project=self.project)
4✔
131

132
            sites = self.cleaned_data.get('sites')
4✔
133
            if sites is not None:
4✔
134
                visibility.sites.set(sites)
4✔
135

136
            groups = self.cleaned_data.get('groups')
4✔
137
            if groups is not None:
4✔
138
                visibility.groups.set(groups)
4✔
139

140
        return self.project
4✔
141

142

143
class ProjectUpdateCatalogForm(forms.ModelForm):
4✔
144

145
    use_required_attribute = False
4✔
146

147
    def __init__(self, *args, **kwargs):
4✔
148
        catalogs = kwargs.pop('catalogs')
4✔
149
        super().__init__(*args, **kwargs)
4✔
150
        self.fields['catalog'].queryset = catalogs
4✔
151
        self.fields['catalog'].empty_label = None
4✔
152

153
    class Meta:
4✔
154
        model = Project
4✔
155
        fields = ('catalog', )
4✔
156
        field_classes = {
4✔
157
            'catalog': CatalogChoiceField
158
        }
159
        widgets = {
4✔
160
            'catalog': forms.RadioSelect()
161
        }
162

163

164
class ProjectUpdateTasksForm(forms.ModelForm):
4✔
165

166
    use_required_attribute = False
4✔
167

168
    def __init__(self, *args, **kwargs):
4✔
169
        tasks = kwargs.pop('tasks')
4✔
170
        super().__init__(*args, **kwargs)
4✔
171
        self.fields['tasks'].queryset = tasks
4✔
172

173
    class Meta:
4✔
174
        model = Project
4✔
175
        fields = ('tasks', )
4✔
176
        field_classes = {
4✔
177
            'tasks': TasksMultipleChoiceField
178
        }
179
        widgets = {
4✔
180
            'tasks': forms.CheckboxSelectMultiple()
181
        }
182

183

184
class ProjectUpdateViewsForm(forms.ModelForm):
4✔
185

186
    use_required_attribute = False
4✔
187

188
    def __init__(self, *args, **kwargs):
4✔
189
        views = kwargs.pop('views')
4✔
190
        super().__init__(*args, **kwargs)
4✔
191
        self.fields['views'].queryset = views
4✔
192

193
    class Meta:
4✔
194
        model = Project
4✔
195
        fields = ('views', )
4✔
196
        field_classes = {
4✔
197
            'views': ViewsMultipleChoiceField
198
        }
199
        widgets = {
4✔
200
            'views': forms.CheckboxSelectMultiple()
201
        }
202

203

204
class ProjectUpdateParentForm(forms.ModelForm):
4✔
205

206
    use_required_attribute = False
4✔
207

208
    def __init__(self, *args, **kwargs):
4✔
209
        projects = kwargs.pop('projects')
4✔
210
        super().__init__(*args, **kwargs)
4✔
211
        self.fields['parent'].queryset = projects
4✔
212

213
    def clean(self):
4✔
214
        ProjectParentValidator(self.instance)(self.cleaned_data)
4✔
215
        super().clean()
4✔
216

217
    class Meta:
4✔
218
        model = Project
4✔
219
        fields = ('parent', )
4✔
220

221

222
class SnapshotCreateForm(forms.ModelForm):
4✔
223

224
    use_required_attribute = False
4✔
225

226
    class Meta:
4✔
227
        model = Snapshot
4✔
228
        fields = ('title', 'description')
4✔
229

230
    def __init__(self, *args, **kwargs):
4✔
231
        self.project = kwargs.pop('project')
4✔
232
        super().__init__(*args, **kwargs)
4✔
233

234
    def save(self, *args, **kwargs):
4✔
235
        self.instance.project = self.project
4✔
236
        return super().save(*args, **kwargs)
4✔
237

238

239
class MembershipCreateForm(forms.Form):
4✔
240

241
    use_required_attribute = False
4✔
242

243
    username_or_email = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('Username or e-mail')}),
4✔
244
                                        label=_('User'),
245
                                        help_text=_('The username or e-mail of the new user.'))
246
    role = forms.CharField(widget=forms.RadioSelect(choices=ROLE_CHOICES),
4✔
247
                           initial='author')
248

249
    def __init__(self, *args, **kwargs):
4✔
250
        self.project = kwargs.pop('project')
4✔
251
        self.is_site_manager = kwargs.pop('is_site_manager')
4✔
252
        super().__init__(*args, **kwargs)
4✔
253

254
        if self.is_site_manager:
4✔
255
            self.fields['silent'] = forms.BooleanField(
4✔
256
                required=False,
257
                label=_('Add member silently'),
258
                help_text=_('As site manager or admin, you can directly add users without notifying them via e-mail, '
259
                            'when you check the following checkbox.')
260
            )
261

262
    def clean_username_or_email(self):
4✔
263
        username_or_email = self.cleaned_data['username_or_email']
4✔
264
        usermodel = get_user_model()
4✔
265

266
        # check if it is a registered user
267
        try:
4✔
268
            self.cleaned_data['user'] = usermodel.objects.get(Q(username=username_or_email) |
4✔
269
                                                              Q(email__iexact=username_or_email))
270
            self.cleaned_data['email'] = self.cleaned_data['user'].email
4✔
271

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

275
        except (usermodel.DoesNotExist, usermodel.MultipleObjectsReturned) as e:
4✔
276
            if settings.PROJECT_SEND_INVITE:
4✔
277
                # check if it is a valid email address, this will raise the correct ValidationError
278
                EmailValidator()(username_or_email)
4✔
279

280
                self.cleaned_data['user'] = None
4✔
281
                self.cleaned_data['email'] = username_or_email
4✔
282
            else:
283
                self.cleaned_data['user'] = None
4✔
284
                self.cleaned_data['email'] = None
4✔
285
                raise ValidationError(_('A user with this username or e-mail was not found. '
4✔
286
                                        'Only registered users can be invited.')) from e
287

288
    def clean(self):
4✔
289
        if self.cleaned_data.get('silent') is True and self.cleaned_data.get('user') is None:
4✔
290
            raise ValidationError(_('Only existing users can be added silently.'))
×
291

292
    def save(self):
4✔
293
        if self.is_site_manager and self.cleaned_data.get('silent') is True:
4✔
294
            Membership.objects.create(
4✔
295
                project=self.project,
296
                user=self.cleaned_data.get('user'),
297
                role=self.cleaned_data.get('role')
298
            )
299
        else:
300
            invite, created = Invite.objects.get_or_create(
4✔
301
                project=self.project,
302
                user=self.cleaned_data.get('user'),
303
                email=self.cleaned_data.get('email')
304
            )
305
            invite.role = self.cleaned_data.get('role')
4✔
306
            invite.make_token()
4✔
307
            invite.save()
4✔
308

309
            return invite
4✔
310

311

312
class IntegrationForm(forms.ModelForm):
4✔
313

314
    class Meta:
4✔
315
        model = Integration
4✔
316
        fields = ()
4✔
317

318
    def __init__(self, *args, **kwargs):
4✔
319
        self.project = kwargs.pop('project')
4✔
320
        self.provider_key = kwargs.pop('provider_key', None)
4✔
321
        super().__init__(*args, **kwargs)
4✔
322

323
        # get the provider
324
        if self.provider_key:
4✔
325
            self.provider = get_plugin('PROJECT_ISSUE_PROVIDERS', self.provider_key)
4✔
326
        else:
327
            self.provider = self.instance.provider
4✔
328

329
        # add fields for the integration options
330
        for field in self.provider.fields:
4✔
331
            # new integration instance is going to be created
332
            if self.instance.pk is None:
4✔
333
                initial = None
4✔
334
            # existing integration is going to be updated
335
            else:
336
                try:
4✔
337
                    initial = IntegrationOption.objects.get(integration=self.instance, key=field.get('key')).value
4✔
338
                except IntegrationOption.DoesNotExist:
×
339
                    initial = None
×
340

341
            if field.get('placeholder'):
4✔
342
                attrs = {'placeholder': field.get('placeholder')}
4✔
343

344
            self.fields[field.get('key')] = forms.CharField(widget=forms.TextInput(attrs=attrs),
4✔
345
                                                            initial=initial, required=field.get('required', True),
346
                                                            help_text=field.get('help'))
347

348
    def save(self):
4✔
349
        # the the project and the provider_key
350
        self.instance.project = self.project
4✔
351
        if self.provider_key:
4✔
352
            self.instance.provider_key = self.provider_key
4✔
353

354
        # call the form's save method
355
        super().save()
4✔
356

357
        # save the integration options
358
        self.instance.save_options(self.cleaned_data)
4✔
359

360

361
class IssueSendForm(forms.Form):
4✔
362

363
    class AttachmentViewsField(forms.ModelMultipleChoiceField):
4✔
364
        def label_from_instance(self, obj):
4✔
365
            return _('Attach %s') % obj.title
4✔
366

367
    class AttachmentFilesField(forms.ModelMultipleChoiceField):
4✔
368
        def label_from_instance(self, obj):
4✔
369
            return _('Attach %s') % obj.file_name
×
370

371
    class AttachmentSnapshotField(forms.ModelChoiceField):
4✔
372
        def label_from_instance(self, obj):
4✔
373
            return obj.title
4✔
374

375
    subject = forms.CharField(label=_('Subject'), max_length=128)
4✔
376
    message = forms.CharField(label=_('Message'), widget=forms.Textarea)
4✔
377

378
    def __init__(self, *args, **kwargs):
4✔
379
        self.project = kwargs.pop('project')
4✔
380
        super().__init__(*args, **kwargs)
4✔
381

382
        self.fields['attachments_answers'] = forms.MultipleChoiceField(
4✔
383
            label=_('Answers'), widget=forms.CheckboxSelectMultiple, required=False,
384
            choices=[('project_answers', _('Attach the output of "View answers".'))]
385
        )
386
        self.fields['attachments_views'] = self.AttachmentViewsField(
4✔
387
            label=_('Views'), widget=forms.CheckboxSelectMultiple, required=False,
388
            queryset=self.project.views.all(), to_field_name='id'
389
        )
390
        self.fields['attachments_files'] = self.AttachmentFilesField(
4✔
391
            label=_('Files'), widget=forms.CheckboxSelectMultiple, required=False,
392
            queryset=self.project.values.filter(snapshot=None)
393
                                        .filter(value_type=VALUE_TYPE_FILE)
394
                                        .order_by('file'),
395
            to_field_name='id'
396
        )
397
        self.fields['attachments_snapshot'] = self.AttachmentSnapshotField(
4✔
398
            label=_('Snapshot'), widget=forms.RadioSelect, required=False,
399
            queryset=self.project.snapshots.all(), empty_label=_('Current')
400
        )
401
        self.fields['attachments_format'] = forms.ChoiceField(
4✔
402
            label=_('Format'), widget=forms.RadioSelect, required=False,
403
            choices=settings.EXPORT_FORMATS
404
        )
405

406
    def clean(self):
4✔
407
        cleaned_data = super().clean()
4✔
408

409
        if cleaned_data.get('attachments_answers') or cleaned_data.get('attachments_views'):
4✔
410
            if not cleaned_data.get('attachments_format'):
4✔
411
                self.add_error('attachments_format', _('This field is required.'))
×
412

413

414
class IssueMailForm(forms.Form):
4✔
415

416
    if settings.EMAIL_RECIPIENTS_CHOICES:
4✔
417
        recipients = forms.MultipleChoiceField(label=_('Recipients'), widget=forms.CheckboxSelectMultiple,
4✔
418
                                               required=not settings.EMAIL_RECIPIENTS_INPUT,
419
                                               choices=settings.EMAIL_RECIPIENTS_CHOICES)
420

421
    if settings.EMAIL_RECIPIENTS_INPUT:
4✔
422
        recipients_input = forms.CharField(label=_('Recipients'), widget=forms.Textarea(attrs={
4✔
423
            'placeholder': _('Enter recipients line by line')
424
        }), required=not settings.EMAIL_RECIPIENTS_CHOICES)
425

426
    def clean(self):
4✔
427
        cleaned_data = super().clean()
4✔
428

429
        if settings.EMAIL_RECIPIENTS_INPUT and \
4✔
430
                cleaned_data.get('recipients') == [] and \
431
                cleaned_data.get('recipients_input') == []:
432
            self.add_error('recipients_input', _('This field is required.'))
4✔
433

434
    def clean_recipients_input(self):
4✔
435
        email_validator = EmailValidator()
4✔
436
        cleaned_data = []
4✔
437

438
        for line in self.cleaned_data['recipients_input'].splitlines():
4✔
439
            email = line.strip()
×
440
            email_validator(email)
×
441
            cleaned_data.append(email)
×
442

443
        return cleaned_data
4✔
444

445

446
class UploadFileForm(forms.Form):
4✔
447
    uploaded_file = forms.FileField(
4✔
448
        label='Select a file',
449
    )
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