forked from ietf-tools/datatracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathforms.py
More file actions
297 lines (255 loc) · 13.1 KB
/
forms.py
File metadata and controls
297 lines (255 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# Copyright The IETF Trust 2013-2025, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import debug #pyflakes:ignore
from django import forms
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import validate_email
from ietf.doc.fields import SearchableDocumentField, SearchableDocumentsField
from ietf.doc.models import RelatedDocument, DocExtResource, State
from ietf.iesg.models import TelechatDate
from ietf.iesg.utils import telechat_page_count
from ietf.person.fields import SearchablePersonField, SearchablePersonsField
from ietf.person.models import Email, Person
from ietf.name.models import ExtResourceName
from ietf.utils.timezone import date_today
from ietf.utils.validators import validate_external_resource_value
class TelechatForm(forms.Form):
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, help_text="Page counts are the current page counts for the telechat, before this telechat date edit is made.")
returning_item = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
dates = [d.date for d in TelechatDate.objects.active().order_by('date')]
init = kwargs['initial'].get("telechat_date")
if init and init not in dates:
dates.insert(0, init)
self.page_count = {}
choice_display = {}
for d in dates:
self.page_count[d] = telechat_page_count(date=d).for_approval
choice_display[d] = '%s (%s pages)' % (d.strftime("%Y-%m-%d"),self.page_count[d])
if d - date_today() < datetime.timedelta(days=13):
choice_display[d] += ' : WARNING - this may not leave enough time for directorate reviews!'
self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, choice_display[d]) for d in dates]
class DocAuthorForm(forms.Form):
person = SearchablePersonField()
email = forms.ModelChoiceField(queryset=Email.objects.none(), required=False)
affiliation = forms.CharField(max_length=100, required=False)
country = forms.CharField(max_length=255, required=False)
def __init__(self, *args, **kwargs):
super(DocAuthorForm, self).__init__(*args, **kwargs)
person = self.data.get(
self.add_prefix('person'),
self.get_initial_for_field(self.fields['person'], 'person')
)
if person:
self.fields['email'].queryset = Email.objects.filter(person=person)
class DocAuthorChangeBasisForm(forms.Form):
basis = forms.CharField(max_length=255,
label='Reason for change',
help_text='What is the source or reasoning for the changes to the author list?')
class AdForm(forms.Form):
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type='area').order_by('name'),
label="Shepherding AD", empty_label="(None)", required=True)
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
# if previous AD is now ex-AD, append that person to the list
ad_pk = self.initial.get('ad')
choices = self.fields['ad'].choices
if ad_pk and ad_pk not in [pk for pk, name in choices]:
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
class NotifyForm(forms.Form):
notify = forms.CharField(
widget=forms.Textarea,
max_length=1023,
help_text="List of email addresses to receive state notifications, separated by comma.",
label="Notification list",
required=False,
)
def clean_notify(self):
# As long as the widget is a Textarea, users will separate addresses with newlines, whether that matches the instructions or not
# We have been allowing nameaddrs for a long time (there are many Documents with namaddrs in their notify field)
# python set doesn't preserve order, so in an attempt to mostly preserve the order of what was entered, we'll use
# a dict (whose keys are guaranteed to be ordered) to cull out duplicates
nameaddrs=dict()
duplicate_addrspecs = set()
bad_nameaddrs = []
for nameaddr in self.cleaned_data["notify"].replace("\n", ",").split(","):
stripped = nameaddr.strip()
if stripped == "":
continue
if "<" in stripped:
if stripped[-1] != ">":
bad_nameaddrs.append(nameaddr)
continue
addrspec = stripped[stripped.find("<")+1:-1]
else:
addrspec = stripped
try:
validate_email(addrspec)
except ValidationError:
bad_nameaddrs.append(nameaddr)
if addrspec in nameaddrs:
duplicate_addrspecs.add(addrspec)
continue
else:
nameaddrs[addrspec] = stripped
error_messages = []
if len(duplicate_addrspecs) != 0:
error_messages.append(f'Duplicate addresses: {", ".join(duplicate_addrspecs)}')
if len(bad_nameaddrs) != 0:
error_messages.append(f'Invalid addresses: {", ".join(bad_nameaddrs)}')
if len(error_messages) != 0:
raise ValidationError(" and ".join(error_messages))
return ", ".join(nameaddrs.values())
class ActionHoldersForm(forms.Form):
action_holders = SearchablePersonsField(required=False)
reason = forms.CharField(
label='Reason for change',
required=False,
max_length=255,
strip=True,
)
IESG_APPROVED_STATE_LIST = ("ann", "rfcqueue", "pub")
class AddDownrefForm(forms.Form):
rfc = SearchableDocumentField(
label="Referenced RFC",
help_text="The RFC that is approved for downref",
required=True,
doc_type="rfc")
drafts = SearchableDocumentsField(
label="Internet-Drafts that makes the reference",
help_text="The Internet-Drafts that approve the downref in their Last Call",
required=True)
def clean_rfc(self):
if 'rfc' not in self.cleaned_data:
raise forms.ValidationError("Please provide a referenced RFC and a referencing Internet-Draft")
rfc = self.cleaned_data['rfc']
if rfc.type_id != "rfc":
raise forms.ValidationError("Cannot find the RFC: " + rfc.name)
return rfc
def clean_drafts(self):
if 'drafts' not in self.cleaned_data:
raise forms.ValidationError("Please provide a referenced RFC and a referencing Internet-Draft")
v_err_names = []
drafts = self.cleaned_data['drafts']
for d in drafts:
state = d.get_state("draft-iesg")
if not state or state.slug not in IESG_APPROVED_STATE_LIST:
v_err_names.append(d.name)
if v_err_names:
raise forms.ValidationError("Internet-Draft is not yet approved: " + ", ".join(v_err_names))
return drafts
def clean(self):
if 'rfc' not in self.cleaned_data or 'drafts' not in self.cleaned_data:
raise forms.ValidationError("Please provide a referenced RFC and a referencing Internet-Draft")
v_err_pairs = []
rfc = self.cleaned_data['rfc']
drafts = self.cleaned_data['drafts']
for d in drafts:
if RelatedDocument.objects.filter(source=d, target=rfc, relationship_id='downref-approval'):
v_err_pairs.append(f"{d.name} --> RFC {rfc.rfc_number}")
if v_err_pairs:
raise forms.ValidationError("Downref is already in the registry: " + ", ".join(v_err_pairs))
if 'save_downref_anyway' not in self.data:
# this check is skipped if the save_downref_anyway button is used
v_err_refnorm = ""
for d in drafts:
if not RelatedDocument.objects.filter(source=d, target=rfc, relationship_id='refnorm'):
if v_err_refnorm:
v_err_refnorm = v_err_refnorm + " or " + d.name
else:
v_err_refnorm = d.name
if v_err_refnorm:
v_err_refnorm_prefix = f"There does not seem to be a normative reference to RFC {rfc.rfc_number} by "
raise forms.ValidationError(v_err_refnorm_prefix + v_err_refnorm)
class ExtResourceForm(forms.Form):
resources = forms.CharField(widget=forms.Textarea, label="Additional Resources", required=False,
help_text=("Format: 'tag value (Optional description)'."
" Separate multiple entries with newline. When the value is a URL, use https:// where possible.") )
def __init__(self, *args, initial=None, extresource_model=None, **kwargs):
self.extresource_model = extresource_model
if initial:
kwargs = kwargs.copy()
resources = initial.get('resources')
if resources is not None and not isinstance(resources, str):
initial = initial.copy()
# Convert objects to string representation
initial['resources'] = self.format_resources(resources)
kwargs['initial'] = initial
super(ExtResourceForm, self).__init__(*args, **kwargs)
@staticmethod
def format_resources(resources, fs="\n"):
# Might be better to shift to a formset instead of parsing these lines.
return fs.join([r.to_form_entry_str() for r in resources])
def clean_resources(self):
"""Clean the resources field
The resources field is a newline-separated set of resource entries. Each entry
should be "<tag> <value>" or "<tag> <value> (<display name>)" with any whitespace
delimiting the components. This clean only validates that the tag and value are
present and valid - tag must be a recognized ExtResourceName and value is
validated using validate_external_resource_value(). Further interpretation of
the resource is performed int he clean() method.
"""
lines = [x.strip() for x in self.cleaned_data["resources"].splitlines() if x.strip()]
errors = []
for l in lines:
parts = l.split()
if len(parts) == 1:
errors.append("Too few fields: Expected at least tag and value: '%s'" % l)
elif len(parts) >= 2:
name_slug = parts[0]
try:
name = ExtResourceName.objects.get(slug=name_slug)
except ObjectDoesNotExist:
errors.append("Bad tag in '%s': Expected one of %s" % (l, ', '.join([ o.slug for o in ExtResourceName.objects.all() ])))
continue
value = parts[1]
try:
validate_external_resource_value(name, value)
except ValidationError as e:
e.message += " : " + value
errors.append(e)
if errors:
raise ValidationError(errors)
return lines
def clean(self):
"""Clean operations after all other fields are cleaned by clean_<field> methods
Converts resource strings into ExtResource model instances.
"""
cleaned_data = super(ExtResourceForm, self).clean()
cleaned_resources = []
cls = self.extresource_model or DocExtResource
for crs in cleaned_data.get('resources', []):
cleaned_resources.append(
cls.from_form_entry_str(crs)
)
cleaned_data['resources'] = cleaned_resources
@staticmethod
def valid_resource_tags():
return ExtResourceName.objects.all().order_by('slug').values_list('slug', flat=True)
class InvestigateForm(forms.Form):
name_fragment = forms.CharField(
label="File name or fragment to investigate",
required=True,
help_text=(
"Enter a filename such as draft-ietf-some-draft-00.txt or a fragment like draft-ietf-some-draft using at least 8 characters. The search will also work for files that are not necessarily drafts."
),
min_length=8,
)
task_id = forms.CharField(required=False, widget=forms.HiddenInput)
def clean_name_fragment(self):
disallowed_characters = ["%", "/", "\\", "*"]
name_fragment = self.cleaned_data["name_fragment"]
# Manual inspection of the directories at the time of this writing shows
# looking for files with less than 8 characters in the name is not useful
# Requiring this will help protect against the secretariat unintentionally
# matching every draft.
if any(c in name_fragment for c in disallowed_characters):
raise ValidationError(f"The following characters are disallowed: {', '.join(disallowed_characters)}")
return name_fragment
class ChangeStatementStateForm(forms.Form):
state = forms.ModelChoiceField(
State.objects.filter(used=True, type="statement"),
empty_label=None,
)