Skip to content

Commit f380b1a

Browse files
authored
ci: merge main to release (#9123)
2 parents e16d568 + 3252e0f commit f380b1a

File tree

13 files changed

+250
-84
lines changed

13 files changed

+250
-84
lines changed

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
"dbaeumer.vscode-eslint",
2424
"eamodio.gitlens",
2525
"editorconfig.editorconfig",
26-
"vue.volar",
26+
// Newer volar >=3.0.0 causes crashes in devcontainers
27+
2728
"mrmlnc.vscode-duplicate",
2829
"ms-azuretools.vscode-docker",
2930
"ms-playwright.playwright",

client/agenda/AgendaScheduleList.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ const meetingEvents = computed(() => {
302302
icon: 'collection',
303303
href: undefined,
304304
click: () => showMaterials(item.id),
305-
color: 'black'
305+
color: 'darkgray'
306306
})
307307
links.push({
308308
id: `lnk-${item.id}-tar`,
@@ -1155,7 +1155,7 @@ onBeforeUnmount(() => {
11551155
.agenda-table-cell-links-buttons {
11561156
white-space: nowrap;
11571157
1158-
> a, > i {
1158+
> a, > i, > button {
11591159
margin-left: 3px;
11601160
color: #666;
11611161
cursor: pointer;
@@ -1197,6 +1197,18 @@ onBeforeUnmount(() => {
11971197
background-color: rgba($orange-500, .3);
11981198
}
11991199
}
1200+
&.text-darkgray {
1201+
color: $gray-900;
1202+
background-color: rgba($gray-700, .1);
1203+
1204+
@at-root .theme-dark & {
1205+
color: $gray-100;
1206+
}
1207+
1208+
&:hover, &:focus {
1209+
background-color: rgba($gray-700, .3);
1210+
}
1211+
}
12001212
&.text-blue {
12011213
color: $blue-600;
12021214
background-color: rgba($blue-300, .1);

client/shared/xslugify.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import slugify from 'slugify'
22

33
export default (str) => {
4-
return slugify(str.replaceAll('/', '-'), { lower: true })
4+
return slugify(str.replaceAll('/', '-').replaceAll(/['&]/g, ''), { lower: true })
55
}

dev/build/celery-start.sh

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,29 @@
55
echo "Running Datatracker checks..."
66
./ietf/manage.py check
77

8-
if ! ietf/manage.py migrate --check ; then
8+
# Check whether the blobdb database exists - inspectdb will return a false
9+
# status if not.
10+
if ietf/manage.py inspectdb --database blobdb > /dev/null 2>&1; then
11+
HAVE_BLOBDB="yes"
12+
fi
13+
14+
migrations_applied_for () {
15+
local DATABASE=${1:-default}
16+
ietf/manage.py migrate --check --database "$DATABASE"
17+
}
18+
19+
migrations_all_applied () {
20+
if [[ "$HAVE_BLOBDB" == "yes" ]]; then
21+
migrations_applied_for default && migrations_applied_for blobdb
22+
else
23+
migrations_applied_for default
24+
fi
25+
}
26+
27+
if ! migrations_all_applied; then
928
echo "Unapplied migrations found, waiting to start..."
1029
sleep 5
11-
while ! ietf/manage.py migrate --check ; do
30+
while ! migrations_all_applied ; do
1231
echo "... still waiting for migrations..."
1332
sleep 5
1433
done

dev/build/datatracker-start.sh

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,29 @@
33
echo "Running Datatracker checks..."
44
./ietf/manage.py check
55

6-
if ! ietf/manage.py migrate --check ; then
6+
# Check whether the blobdb database exists - inspectdb will return a false
7+
# status if not.
8+
if ietf/manage.py inspectdb --database blobdb > /dev/null 2>&1; then
9+
HAVE_BLOBDB="yes"
10+
fi
11+
12+
migrations_applied_for () {
13+
local DATABASE=${1:-default}
14+
ietf/manage.py migrate --check --database "$DATABASE"
15+
}
16+
17+
migrations_all_applied () {
18+
if [[ "$HAVE_BLOBDB" == "yes" ]]; then
19+
migrations_applied_for default && migrations_applied_for blobdb
20+
else
21+
migrations_applied_for default
22+
fi
23+
}
24+
25+
if ! migrations_all_applied; then
726
echo "Unapplied migrations found, waiting to start..."
827
sleep 5
9-
while ! ietf/manage.py migrate --check ; do
28+
while ! migrations_all_applied ; do
1029
echo "... still waiting for migrations..."
1130
sleep 5
1231
done

ietf/api/tests.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1582,16 +1582,17 @@ def test_bad_post(self):
15821582
data = self.response_data(r)
15831583
self.assertEqual(data["result"], "failure")
15841584
self.assertEqual(data["reason"], "invalid post")
1585-
1585+
15861586
bad = dict(authtoken=self.valid_token, username=self.valid_person.user.username, password=self.valid_password)
15871587
r = self.client.post(self.url, bad)
15881588
self.assertEqual(r.status_code, 200)
15891589
data = self.response_data(r)
15901590
self.assertEqual(data["result"], "failure")
15911591
self.assertEqual(data["reason"], "invalid post")
15921592

1593+
@override_settings()
15931594
def test_notokenstore(self):
1594-
self.assertFalse(hasattr(settings, "APP_API_TOKENS"))
1595+
del settings.APP_API_TOKENS # only affects overridden copy of settings!
15951596
r = self.client.post(self.url,self.valid_body_with_good_password)
15961597
self.assertEqual(r.status_code, 200)
15971598
data = self.response_data(r)

ietf/doc/templatetags/ietf_filters.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def urlize_related_source_list(related, document_html=False):
285285
url=url)
286286
))
287287
return links
288-
288+
289289
@register.filter(name='urlize_related_target_list', is_safe=True, document_html=False)
290290
def urlize_related_target_list(related, document_html=False):
291291
"""Convert a list of RelatedDocuments into list of links using the target document's canonical name"""
@@ -302,7 +302,7 @@ def urlize_related_target_list(related, document_html=False):
302302
url=url)
303303
))
304304
return links
305-
305+
306306
@register.filter(name='dashify')
307307
def dashify(string):
308308
"""
@@ -521,10 +521,52 @@ def plural(text, seq, arg='s'):
521521
else:
522522
return text + pluralize(len(seq), arg)
523523

524+
525+
# Translation table to escape ICS characters. The {} | {} construction builds up a dict
526+
# mapping characters to arbitrary-length strings or None. Values in later dicts override
527+
# earlier ones prior to conversion to a translation table, so excluding a char and then
528+
# mapping it to an escape sequence results in its being escaped, not dropped.
529+
rfc5545_text_escapes = str.maketrans(
530+
# text = *(TSAFE-CHAR / ":" / DQUOTE / ESCAPED-CHAR)
531+
# TSAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-5B /
532+
# %x5D-7E / NON-US-ASCII
533+
{chr(c): None for c in range(0x00, 0x20)} # strip 0x00-0x20
534+
| {
535+
# ESCAPED-CHAR = ("\\" / "\;" / "\," / "\N" / "\n")
536+
"\n": r"\n",
537+
";": r"\;",
538+
",": r"\,",
539+
"\\": r"\\", # rhs is two backslashes!
540+
"\t": "\t", # htab ok (0x09)
541+
" ": " ", # space ok (0x20)
542+
}
543+
)
544+
545+
524546
@register.filter
525547
def ics_esc(text):
526-
text = re.sub(r"([\n,;\\])", r"\\\1", text)
527-
return text
548+
"""Escape a string to use in an iCalendar text context
549+
550+
>>> ics_esc('simple')
551+
'simple'
552+
553+
For the next tests, it helps to know:
554+
chr(0x09) = "\t"
555+
chr(0x0a) = "\n"
556+
chr(0x0d) = "\r"
557+
chr(0x5c) = "\\"
558+
559+
>>> ics_esc(f'strips{chr(0x0d)}out{chr(0x0d)}LFs')
560+
'stripsoutLFs'
561+
562+
563+
>>> ics_esc(f'escapes;and,and{chr(0x5c)}and{chr(0x0a)}')
564+
'escapes\\\\;and\\\\,and\\\\\\\\and\\\\n'
565+
566+
>>> ics_esc(f"keeps spaces : and{chr(0x09)}tabs")
567+
'keeps spaces : and\\ttabs'
568+
"""
569+
return text.translate(rfc5545_text_escapes)
528570

529571

530572
@register.simple_tag
@@ -557,7 +599,7 @@ def ics_date_time(dt, tzname):
557599
return f':{timestamp}Z'
558600
else:
559601
return f';TZID={ics_esc(tzname)}:{timestamp}'
560-
602+
561603
@register.filter
562604
def next_day(value):
563605
return value + datetime.timedelta(days=1)
@@ -676,7 +718,7 @@ def rfcbis(s):
676718
@stringfilter
677719
def urlize(value):
678720
raise RuntimeError("Use linkify from textfilters instead of urlize")
679-
721+
680722
@register.filter
681723
@stringfilter
682724
def charter_major_rev(rev):

ietf/ietfauth/forms.py

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,56 @@ def clean_email(self):
3333
return email
3434

3535

36+
class PasswordStrengthField(forms.CharField):
37+
widget = PasswordStrengthInput(
38+
attrs={
39+
"class": "password_strength",
40+
"data-disable-strength-enforcement": "", # usually removed in init
41+
}
42+
)
43+
44+
def __init__(self, *args, **kwargs):
45+
super().__init__(*args, **kwargs)
46+
for pwval in password_validation.get_default_password_validators():
47+
if isinstance(pwval, password_validation.MinimumLengthValidator):
48+
self.widget.attrs["minlength"] = pwval.min_length
49+
elif isinstance(pwval, StrongPasswordValidator):
50+
self.widget.attrs.pop(
51+
"data-disable-strength-enforcement", None
52+
)
53+
54+
55+
3656
class PasswordForm(forms.Form):
37-
password = forms.CharField(widget=PasswordStrengthInput(attrs={'class':'password_strength'}))
57+
password = PasswordStrengthField()
3858
password_confirmation = forms.CharField(widget=PasswordConfirmationInput(
3959
confirm_with='password',
4060
attrs={'class':'password_confirmation'}),
4161
help_text="Enter the same password as above, for verification.",)
42-
62+
63+
def __init__(self, *args, user=None, **kwargs):
64+
# user is a kw-only argument to avoid interfering with the signature
65+
# when this class is mixed with ModelForm in PersonPasswordForm
66+
self.user = user
67+
super().__init__(*args, **kwargs)
4368

4469
def clean_password_confirmation(self):
45-
password = self.cleaned_data.get("password", "")
46-
password_confirmation = self.cleaned_data["password_confirmation"]
70+
# clean fields here rather than a clean() method so validation is
71+
# still enforced in PersonPasswordForm without having to override its
72+
# clean() method
73+
password = self.cleaned_data.get("password")
74+
password_confirmation = self.cleaned_data.get("password_confirmation")
4775
if password != password_confirmation:
48-
raise forms.ValidationError("The two password fields didn't match.")
76+
raise ValidationError(
77+
"The password confirmation is different than the new password"
78+
)
79+
try:
80+
password_validation.validate_password(password_confirmation, self.user)
81+
except ValidationError as err:
82+
self.add_error("password", err)
4983
return password_confirmation
5084

85+
5186
def ascii_cleaner(supposedly_ascii):
5287
outside_printable_ascii_pattern = r'[^\x20-\x7F]'
5388
if re.search(outside_printable_ascii_pattern, supposedly_ascii):
@@ -174,35 +209,13 @@ class Meta:
174209
exclude = ['by', 'time' ]
175210

176211

177-
class ChangePasswordForm(forms.Form):
212+
class ChangePasswordForm(PasswordForm):
178213
current_password = forms.CharField(widget=forms.PasswordInput)
214+
field_order = ["current_password", "password", "password_confirmation"]
179215

180-
new_password = forms.CharField(
181-
widget=PasswordStrengthInput(
182-
attrs={
183-
"class": "password_strength",
184-
"data-disable-strength-enforcement": "", # usually removed in init
185-
}
186-
),
187-
)
188-
new_password_confirmation = forms.CharField(
189-
widget=PasswordConfirmationInput(
190-
confirm_with="new_password", attrs={"class": "password_confirmation"}
191-
)
192-
)
193-
194-
def __init__(self, user, data=None):
195-
self.user = user
196-
super().__init__(data)
197-
# Check whether we have validators to enforce
198-
new_password_field = self.fields["new_password"]
199-
for pwval in password_validation.get_default_password_validators():
200-
if isinstance(pwval, password_validation.MinimumLengthValidator):
201-
new_password_field.widget.attrs["minlength"] = pwval.min_length
202-
elif isinstance(pwval, StrongPasswordValidator):
203-
new_password_field.widget.attrs.pop(
204-
"data-disable-strength-enforcement", None
205-
)
216+
def __init__(self, user, *args, **kwargs):
217+
# user arg is optional in superclass, but required for this form
218+
super().__init__(*args, user=user, **kwargs)
206219

207220
def clean_current_password(self):
208221
# n.b., password = None is handled by check_password and results in a failed check
@@ -211,15 +224,6 @@ def clean_current_password(self):
211224
raise ValidationError("Invalid password")
212225
return password
213226

214-
def clean(self):
215-
new_password = self.cleaned_data.get("new_password", "")
216-
conf_password = self.cleaned_data.get("new_password_confirmation", "")
217-
if new_password != conf_password:
218-
raise ValidationError(
219-
"The password confirmation is different than the new password"
220-
)
221-
password_validation.validate_password(conf_password, self.user)
222-
223227

224228
class ChangeUsernameForm(forms.Form):
225229
username = forms.ChoiceField(choices=[('-','--------')])

0 commit comments

Comments
 (0)