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

rdmorganiser / rdmo / 20428555173

22 Dec 2025 10:02AM UTC coverage: 94.693% (-0.1%) from 94.814%
20428555173

Pull #1436

github

web-flow
Merge 0ffb48a5d into 57a75b09e
Pull Request #1436: Draft: add `rdmo.config` app for `Plugin` model (plugin managament)

2191 of 2304 branches covered (95.1%)

23411 of 24723 relevant lines covered (94.69%)

3.79 hits per line

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

94.37
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.http import Http404
4✔
9
from django.utils.safestring import mark_safe
4✔
10
from django.utils.translation import gettext_lazy as _
4✔
11

12
from rdmo.config.constants import PluginType
4✔
13
from rdmo.config.models import Plugin
4✔
14
from rdmo.core.constants import VALUE_TYPE_FILE
4✔
15
from rdmo.core.utils import markdown2html
4✔
16

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

21

22
class CatalogChoiceField(forms.ModelChoiceField):
4✔
23

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

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

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

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

36

37
class TasksMultipleChoiceField(forms.ModelMultipleChoiceField):
4✔
38

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

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

45

46
class ViewsMultipleChoiceField(forms.ModelMultipleChoiceField):
4✔
47

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

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

54

55
class ProjectForm(forms.ModelForm):
4✔
56

57
    use_required_attribute = False
4✔
58

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

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

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

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

80
    class Meta:
4✔
81
        model = Project
4✔
82

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

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

94

95
class ProjectUpdateInformationForm(forms.ModelForm):
4✔
96

97
    use_required_attribute = False
4✔
98

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

103

104
class ProjectUpdateVisibilityForm(forms.ModelForm):
4✔
105

106
    use_required_attribute = False
4✔
107

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

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

118
        super().__init__(*args, instance=instance, **kwargs)
4✔
119

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

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

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

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

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

151
        return self.project
4✔
152

153

154
class ProjectUpdateCatalogForm(forms.ModelForm):
4✔
155

156
    use_required_attribute = False
4✔
157

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

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

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

180

181
class ProjectUpdateTasksForm(forms.ModelForm):
4✔
182

183
    use_required_attribute = False
4✔
184

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

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

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

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

208

209
class ProjectUpdateViewsForm(forms.ModelForm):
4✔
210

211
    use_required_attribute = False
4✔
212

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

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

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

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

236

237
class ProjectUpdateParentForm(forms.ModelForm):
4✔
238

239
    use_required_attribute = False
4✔
240

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

246
    def clean(self):
4✔
247
        ProjectParentValidator(self.instance)(self.cleaned_data)
4✔
248
        super().clean()
4✔
249

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

254

255
class SnapshotCreateForm(forms.ModelForm):
4✔
256

257
    use_required_attribute = False
4✔
258

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

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

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

271

272
class MembershipCreateForm(forms.Form):
4✔
273

274
    use_required_attribute = False
4✔
275

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

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

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

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

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

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

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

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

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

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

342
            return invite
4✔
343

344

345
class IntegrationForm(forms.ModelForm):
4✔
346

347
    class Meta:
4✔
348
        model = Integration
4✔
349
        fields = ()
4✔
350

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

356
        # get the provider
357
        if self.provider_key:
4✔
358

359
            plugins = Plugin.objects.for_context(
4✔
360
                plugin_type=PluginType.PROJECT_ISSUE_PROVIDER, project=self.project, format=self.provider_key
361
            )
362
            if not plugins.exists():
4✔
363
                raise Http404
×
364
            self.provider = plugins.first().initialize_class()
4✔
365
        else:
366
            self.provider = self.instance.provider
4✔
367
            if self.provider is None:
4✔
368
                raise Http404
×
369

370
        # add fields for the integration options
371
        for field in self.provider.fields:
4✔
372
            # new integration instance is going to be created
373
            if self.instance.pk is None:
4✔
374
                initial = None
4✔
375
            # existing integration is going to be updated
376
            else:
377
                try:
4✔
378
                    initial = IntegrationOption.objects.get(integration=self.instance, key=field.get('key')).value
4✔
379
                except IntegrationOption.DoesNotExist:
×
380
                    initial = None
×
381

382
            if field.get('placeholder'):
4✔
383
                attrs = {'placeholder': field.get('placeholder')}
4✔
384

385
            self.fields[field.get('key')] = forms.CharField(widget=forms.TextInput(attrs=attrs),
4✔
386
                                                            initial=initial, required=field.get('required', True),
387
                                                            help_text=field.get('help'))
388

389
    def save(self):
4✔
390
        # the the project and the provider_key
391
        self.instance.project = self.project
4✔
392
        if self.provider_key:
4✔
393
            self.instance.provider_key = self.provider_key
4✔
394

395
        # call the form's save method
396
        super().save()
4✔
397

398
        # save the integration options
399
        self.instance.save_options(self.cleaned_data)
4✔
400

401

402
class IssueSendForm(forms.Form):
4✔
403

404
    class AttachmentViewsField(forms.ModelMultipleChoiceField):
4✔
405
        def label_from_instance(self, obj):
4✔
406
            return _('Attach %s') % obj.title
4✔
407

408
    class AttachmentFilesField(forms.ModelMultipleChoiceField):
4✔
409
        def label_from_instance(self, obj):
4✔
410
            return _('Attach %s') % obj.file_name
×
411

412
    class AttachmentSnapshotField(forms.ModelChoiceField):
4✔
413
        def label_from_instance(self, obj):
4✔
414
            return obj.title
4✔
415

416
    subject = forms.CharField(label=_('Subject'), max_length=128)
4✔
417
    message = forms.CharField(label=_('Message'), widget=forms.Textarea)
4✔
418

419
    def __init__(self, *args, **kwargs):
4✔
420
        self.project = kwargs.pop('project')
4✔
421
        super().__init__(*args, **kwargs)
4✔
422

423
        self.fields['attachments_answers'] = forms.MultipleChoiceField(
4✔
424
            label=_('Answers'), widget=forms.CheckboxSelectMultiple, required=False,
425
            choices=[('project_answers', _('Attach the output of "View answers".'))]
426
        )
427
        self.fields['attachments_views'] = self.AttachmentViewsField(
4✔
428
            label=_('Views'), widget=forms.CheckboxSelectMultiple, required=False,
429
            queryset=self.project.views.all(), to_field_name='id'
430
        )
431
        self.fields['attachments_files'] = self.AttachmentFilesField(
4✔
432
            label=_('Files'), widget=forms.CheckboxSelectMultiple, required=False,
433
            queryset=self.project.values.filter(snapshot=None)
434
                                        .filter(value_type=VALUE_TYPE_FILE)
435
                                        .order_by('file'),
436
            to_field_name='id'
437
        )
438
        self.fields['attachments_snapshot'] = self.AttachmentSnapshotField(
4✔
439
            label=_('Snapshot'), widget=forms.RadioSelect, required=False,
440
            queryset=self.project.snapshots.all(), empty_label=_('Current')
441
        )
442
        self.fields['attachments_format'] = forms.ChoiceField(
4✔
443
            label=_('Format'), widget=forms.RadioSelect, required=False,
444
            choices=settings.EXPORT_FORMATS
445
        )
446

447
    def clean(self):
4✔
448
        cleaned_data = super().clean()
4✔
449

450
        if cleaned_data.get('attachments_answers') or cleaned_data.get('attachments_views'):
4✔
451
            if not cleaned_data.get('attachments_format'):
4✔
452
                self.add_error('attachments_format', _('This field is required.'))
×
453

454

455
class IssueMailForm(forms.Form):
4✔
456

457
    if settings.EMAIL_RECIPIENTS_CHOICES:
4✔
458
        recipients = forms.MultipleChoiceField(label=_('Recipients'), widget=forms.CheckboxSelectMultiple,
4✔
459
                                               required=not settings.EMAIL_RECIPIENTS_INPUT,
460
                                               choices=settings.EMAIL_RECIPIENTS_CHOICES)
461

462
    if settings.EMAIL_RECIPIENTS_INPUT:
4✔
463
        recipients_input = forms.CharField(label=_('Recipients'), widget=forms.Textarea(attrs={
4✔
464
            'placeholder': _('Enter recipients line by line')
465
        }), required=not settings.EMAIL_RECIPIENTS_CHOICES)
466

467
    def clean(self):
4✔
468
        cleaned_data = super().clean()
4✔
469

470
        if settings.EMAIL_RECIPIENTS_INPUT and \
4✔
471
                cleaned_data.get('recipients') == [] and \
472
                cleaned_data.get('recipients_input') == []:
473
            self.add_error('recipients_input', _('This field is required.'))
4✔
474

475
    def clean_recipients_input(self):
4✔
476
        email_validator = EmailValidator()
4✔
477
        cleaned_data = []
4✔
478

479
        for line in self.cleaned_data['recipients_input'].splitlines():
4✔
480
            email = line.strip()
×
481
            email_validator(email)
×
482
            cleaned_data.append(email)
×
483

484
        return cleaned_data
4✔
485

486

487
class UploadFileForm(forms.Form):
4✔
488
    uploaded_file = forms.FileField(
4✔
489
        label='Select a file',
490
    )
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