Merge pull request #5629 from jennifer-richards/django32
chore: Upgrade to Django 3.2
This commit is contained in:
commit
0d5ecea42e
ietf
api
community
doc
group
ietfauth
liaisons
meeting
name/management/commands
nomcom
secr
settings.pysubmit
sync
templates
doc
registration
utils
patch
change-oidc-provider-field-sizes-228.patchdjango-cookie-delete-with-all-settings.patchfix-django-password-strength-kwargs.patchfix-oidc-access-token-post.patchtastypie-django22-fielderror-response.patch
requirements.txt
|
@ -11,9 +11,10 @@ from django.core.serializers.json import Serializer
|
|||
from django.http import HttpResponse
|
||||
from django.utils.encoding import smart_str
|
||||
from django.db.models import Field
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.models.signals import post_save, post_delete, m2m_changed
|
||||
|
||||
from django_stubs_ext import QuerySetAny
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
||||
|
@ -145,7 +146,7 @@ class AdminJsonSerializer(Serializer):
|
|||
field_value = None
|
||||
else:
|
||||
field_value = field
|
||||
if isinstance(field_value, QuerySet) or isinstance(field_value, list):
|
||||
if isinstance(field_value, QuerySetAny) or isinstance(field_value, list):
|
||||
self._current[name] = dict([ (rel.pk, self.expand_related(rel, name)) for rel in field_value ])
|
||||
else:
|
||||
if hasattr(field_value, "_meta"):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright The IETF Trust 2017, All Rights Reserved
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.urls import include
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from ietf import api
|
||||
|
|
|
@ -22,6 +22,7 @@ from ietf.community.utils import docs_tracked_by_community_list, docs_matching_c
|
|||
from ietf.community.utils import states_of_significant_change, reset_name_contains_index_for_rule
|
||||
from ietf.doc.models import DocEvent, Document
|
||||
from ietf.doc.utils_search import prepare_document_table
|
||||
from ietf.utils.http import is_ajax
|
||||
from ietf.utils.response import permission_denied
|
||||
|
||||
def view_list(request, username=None):
|
||||
|
@ -142,7 +143,7 @@ def track_document(request, name, username=None, acronym=None):
|
|||
if not doc in clist.added_docs.all():
|
||||
clist.added_docs.add(doc)
|
||||
|
||||
if request.is_ajax():
|
||||
if is_ajax(request):
|
||||
return HttpResponse(json.dumps({ 'success': True }), content_type='application/json')
|
||||
else:
|
||||
return HttpResponseRedirect(clist.get_absolute_url())
|
||||
|
@ -162,7 +163,7 @@ def untrack_document(request, name, username=None, acronym=None):
|
|||
if clist.pk is not None:
|
||||
clist.added_docs.remove(doc)
|
||||
|
||||
if request.is_ajax():
|
||||
if is_ajax(request):
|
||||
return HttpResponse(json.dumps({ 'success': True }), content_type='application/json')
|
||||
else:
|
||||
return HttpResponseRedirect(clist.get_absolute_url())
|
||||
|
|
|
@ -1779,7 +1779,7 @@ class DocTestCase(TestCase):
|
|||
self.client.login(username='ad', password='ad+password')
|
||||
r = self.client.post(urlreverse('ietf.doc.views_status_change.change_state',kwargs=dict(name=doc.name)),dict(new_state=iesgeval_pk))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
r = self.client.get(r._headers["location"][1])
|
||||
r = self.client.get(r.headers["location"])
|
||||
self.assertContains(r, ">IESG Evaluation<")
|
||||
self.assertEqual(len(outbox), 2)
|
||||
self.assertIn('iesg-secretary',outbox[0]['To'])
|
||||
|
|
|
@ -33,9 +33,9 @@
|
|||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
from django.conf.urls import include
|
||||
from django.views.generic import RedirectView
|
||||
from django.conf import settings
|
||||
from django.urls import include
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from ietf.doc import views_search, views_draft, views_ballot, views_status_change, views_doc, views_downref, views_stats, views_help, views_bofreq
|
||||
from ietf.utils.urls import url
|
||||
|
|
|
@ -11,7 +11,7 @@ import requests
|
|||
import email.utils
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.http import is_safe_url
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
|
||||
from simple_history.utils import update_change_reason
|
||||
|
||||
|
@ -53,6 +53,7 @@ from ietf.utils.textupload import get_cleaned_text_file_content
|
|||
from ietf.utils.mail import send_mail_message
|
||||
from ietf.mailtrigger.utils import gather_address_lists
|
||||
from ietf.utils.fields import MultiEmailField
|
||||
from ietf.utils.http import is_ajax
|
||||
from ietf.utils.response import permission_denied
|
||||
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
|
||||
|
||||
|
@ -1087,11 +1088,16 @@ def review_wishes_remove(request, name):
|
|||
|
||||
|
||||
def _generate_ajax_or_redirect_response(request, doc):
|
||||
redirect_url = request.GET.get('next')
|
||||
url_is_safe = is_safe_url(url=redirect_url, allowed_hosts=request.get_host(),
|
||||
require_https=request.is_secure())
|
||||
if request.is_ajax():
|
||||
return HttpResponse(json.dumps({'success': True}), content_type='application/json')
|
||||
redirect_url = request.GET.get("next")
|
||||
url_is_safe = url_has_allowed_host_and_scheme(
|
||||
url=redirect_url,
|
||||
allowed_hosts=request.get_host(),
|
||||
require_https=request.is_secure(),
|
||||
)
|
||||
if is_ajax(request):
|
||||
return HttpResponse(
|
||||
json.dumps({"success": True}), content_type="application/json"
|
||||
)
|
||||
elif url_is_safe:
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
else:
|
||||
|
|
|
@ -25,7 +25,6 @@ from ietf.utils.timezone import date_today
|
|||
|
||||
epochday = datetime.datetime.utcfromtimestamp(0).date().toordinal()
|
||||
|
||||
column_chart_conf = settings.CHART_TYPE_COLUMN_OPTIONS
|
||||
|
||||
def dt(s):
|
||||
"Convert the date string returned by sqlite's date() to a datetime.date"
|
||||
|
|
|
@ -16,7 +16,7 @@ from django.http import Http404
|
|||
from django.shortcuts import render
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
|
||||
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright The IETF Trust 2013-2020, All Rights Reserved
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.urls import include
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from ietf.community import views as community_views
|
||||
|
|
|
@ -11,14 +11,14 @@ from django.core.exceptions import ValidationError
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from django_password_strength.widgets import PasswordStrengthInput, PasswordConfirmationInput
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.mailinglists.models import Allowlisted
|
||||
from ietf.utils.text import isascii
|
||||
|
||||
from .widgets import PasswordStrengthInput, PasswordConfirmationInput
|
||||
|
||||
|
||||
class RegistrationForm(forms.Form):
|
||||
email = forms.EmailField(label="Your email (lowercase)")
|
||||
|
|
|
@ -8,6 +8,7 @@ import oidc_provider.lib.claims
|
|||
|
||||
|
||||
from functools import wraps, WRAPPER_ASSIGNMENTS
|
||||
from urllib.parse import quote as urlquote
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
|
@ -15,7 +16,6 @@ from django.core.exceptions import PermissionDenied
|
|||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.http import urlquote
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
|
114
ietf/ietfauth/widgets.py
Normal file
114
ietf/ietfauth/widgets.py
Normal file
|
@ -0,0 +1,114 @@
|
|||
from django.forms import PasswordInput
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
# The PasswordStrengthInput and PasswordConfirmationInput widgets come from the
|
||||
# django-password-strength project, https://pypi.org/project/django-password-strength/
|
||||
#
|
||||
# Original license:
|
||||
#
|
||||
# Copyright © 2015 A.J. May and individual contributors. All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
|
||||
# following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
|
||||
# disclaimer.
|
||||
#
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
|
||||
# following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
#
|
||||
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
|
||||
# products derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
#
|
||||
|
||||
class PasswordStrengthInput(PasswordInput):
|
||||
"""
|
||||
Form widget to show the user how strong his/her password is.
|
||||
"""
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
strength_markup = """
|
||||
<div style="margin-top: 10px;">
|
||||
<div class="progress" style="margin-bottom: 10px;">
|
||||
<div class="progress-bar progress-bar-warning password_strength_bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="5" style="width: 0%%"></div>
|
||||
</div>
|
||||
<p class="text-muted password_strength_info hidden">
|
||||
<span class="label label-danger">
|
||||
%s
|
||||
</span>
|
||||
<span style="margin-left:5px;">
|
||||
%s
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
""" % (
|
||||
_("Warning"),
|
||||
_(
|
||||
'This password would take <em class="password_strength_time"></em> to crack.'
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
self.attrs["class"] = "%s password_strength".strip() % self.attrs["class"]
|
||||
except KeyError:
|
||||
self.attrs["class"] = "password_strength"
|
||||
|
||||
return mark_safe(
|
||||
super(PasswordInput, self).render(name, value, attrs, renderer)
|
||||
+ strength_markup
|
||||
)
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
"ietf/js/zxcvbn.js",
|
||||
"ietf/js/password_strength.js",
|
||||
)
|
||||
|
||||
|
||||
class PasswordConfirmationInput(PasswordInput):
|
||||
"""
|
||||
Form widget to confirm the users password by letting him/her type it again.
|
||||
"""
|
||||
|
||||
def __init__(self, confirm_with=None, attrs=None, render_value=False):
|
||||
super(PasswordConfirmationInput, self).__init__(attrs, render_value)
|
||||
self.confirm_with = confirm_with
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
if self.confirm_with:
|
||||
self.attrs["data-confirm-with"] = "id_%s" % self.confirm_with
|
||||
|
||||
confirmation_markup = """
|
||||
<div style="margin-top: 10px;" class="hidden password_strength_info">
|
||||
<p class="text-muted">
|
||||
<span class="label label-danger">
|
||||
%s
|
||||
</span>
|
||||
<span style="margin-left:5px;">%s</span>
|
||||
</p>
|
||||
</div>
|
||||
""" % (
|
||||
_("Warning"),
|
||||
_("Your passwords don't match."),
|
||||
)
|
||||
|
||||
try:
|
||||
self.attrs["class"] = (
|
||||
"%s password_confirmation".strip() % self.attrs["class"]
|
||||
)
|
||||
except KeyError:
|
||||
self.attrs["class"] = "password_confirmation"
|
||||
|
||||
return mark_safe(
|
||||
super(PasswordInput, self).render(name, value, attrs, renderer)
|
||||
+ confirmation_markup
|
||||
)
|
|
@ -13,11 +13,11 @@ from email.utils import parseaddr
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.db.models.query import QuerySet
|
||||
from django.forms.utils import ErrorList
|
||||
from django.db.models import Q
|
||||
#from django.forms.widgets import RadioFieldRenderer
|
||||
from django.core.validators import validate_email
|
||||
from django_stubs_ext import QuerySetAny
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
@ -203,7 +203,7 @@ class SearchLiaisonForm(forms.Form):
|
|||
class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
'''If value is a QuerySet, return it as is (for use in widget.render)'''
|
||||
def prepare_value(self, value):
|
||||
if isinstance(value, QuerySet):
|
||||
if isinstance(value, QuerySetAny):
|
||||
return value
|
||||
if (hasattr(value, '__iter__') and
|
||||
not isinstance(value, str) and
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
|
||||
|
||||
from django.urls import reverse as urlreverse
|
||||
from django.db.models.query import QuerySet
|
||||
from django.forms.widgets import Widget
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import conditional_escape
|
||||
|
||||
from django_stubs_ext import QuerySetAny
|
||||
|
||||
|
||||
class ButtonWidget(Widget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -34,7 +35,7 @@ class ShowAttachmentsWidget(Widget):
|
|||
html = '<div id="id_%s">' % name
|
||||
html += '<span class="d-none showAttachmentsEmpty form-control widget">No files attached</span>'
|
||||
html += '<div class="attachedFiles form-control widget">'
|
||||
if value and isinstance(value, QuerySet):
|
||||
if value and isinstance(value, QuerySetAny):
|
||||
for attachment in value:
|
||||
html += '<a class="initialAttach" href="%s">%s</a> ' % (conditional_escape(attachment.document.get_href()), conditional_escape(attachment.document.title))
|
||||
html += '<a class="btn btn-primary btn-sm" href="{}">Edit</a> '.format(urlreverse("ietf.liaisons.views.liaison_edit_attachment", kwargs={'object_id':attachment.statement.pk,'doc_id':attachment.document.pk}))
|
||||
|
@ -43,4 +44,4 @@ class ShowAttachmentsWidget(Widget):
|
|||
else:
|
||||
html += 'No files attached'
|
||||
html += '</div></div>'
|
||||
return mark_safe(html)
|
||||
return mark_safe(html)
|
||||
|
|
|
@ -1493,7 +1493,7 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
|
|||
"""Test the timeslot editor"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.meeting: Meeting = MeetingFactory(
|
||||
self.meeting: Meeting = MeetingFactory( # type: ignore[annotation-unchecked]
|
||||
type_id='ietf',
|
||||
number=120,
|
||||
date=date_today() + datetime.timedelta(days=10),
|
||||
|
@ -1570,13 +1570,13 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
|
|||
delete_time = delete_time_local.astimezone(datetime.timezone.utc)
|
||||
duration = datetime.timedelta(minutes=60)
|
||||
|
||||
delete: [TimeSlot] = TimeSlotFactory.create_batch(
|
||||
delete: [TimeSlot] = TimeSlotFactory.create_batch( # type: ignore[annotation-unchecked]
|
||||
2,
|
||||
meeting=self.meeting,
|
||||
time=delete_time_local,
|
||||
duration=duration,
|
||||
)
|
||||
keep: [TimeSlot] = [
|
||||
keep: [TimeSlot] = [ # type: ignore[annotation-unchecked]
|
||||
TimeSlotFactory(
|
||||
meeting=self.meeting,
|
||||
time=keep_time,
|
||||
|
@ -1613,14 +1613,14 @@ class EditTimeslotsTests(IetfSeleniumTestCase):
|
|||
hours = [10, 12]
|
||||
other_days = [self.meeting.get_meeting_date(d) for d in range(1, 3)]
|
||||
|
||||
delete: [TimeSlot] = [
|
||||
delete: [TimeSlot] = [ # type: ignore[annotation-unchecked]
|
||||
TimeSlotFactory(
|
||||
meeting=self.meeting,
|
||||
time=datetime_from_date(delete_day, self.meeting.tz()).replace(hour=hour),
|
||||
) for hour in hours
|
||||
]
|
||||
|
||||
keep: [TimeSlot] = [
|
||||
keep: [TimeSlot] = [ # type: ignore[annotation-unchecked]
|
||||
TimeSlotFactory(
|
||||
meeting=self.meeting,
|
||||
time=datetime_from_date(day, self.meeting.tz()).replace(hour=hour),
|
||||
|
|
|
@ -556,7 +556,7 @@ class MeetingTests(BaseMeetingTestCase):
|
|||
self.assertContains(r, "1. More work items underway")
|
||||
|
||||
|
||||
cont_disp = r._headers.get('content-disposition', ('Content-Disposition', ''))[1]
|
||||
cont_disp = r.headers.get('content-disposition', ('Content-Disposition', ''))[1]
|
||||
cont_disp = re.split('; ?', cont_disp)
|
||||
cont_disp_settings = dict( e.split('=', 1) for e in cont_disp if '=' in e )
|
||||
filename = cont_disp_settings.get('filename', '').strip('"')
|
||||
|
@ -2357,7 +2357,7 @@ class EditTimeslotsTests(TestCase):
|
|||
|
||||
def test_invalid_edit_timeslot(self):
|
||||
meeting = self.create_bare_meeting()
|
||||
ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # n.b., colon indicates type hinting
|
||||
ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # type: ignore[annotation-unchecked]
|
||||
self.login()
|
||||
r = self.client.post(
|
||||
self.edit_timeslot_url(ts),
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright The IETF Trust 2007-2020, All Rights Reserved
|
||||
|
||||
from django.conf.urls import include
|
||||
from django.views.generic import RedirectView
|
||||
from django.conf import settings
|
||||
from django.urls import include
|
||||
from django.views.generic import RedirectView
|
||||
|
||||
from ietf.meeting import views, views_proceedings
|
||||
from ietf.utils.urls import url
|
||||
|
|
|
@ -67,7 +67,7 @@ class Command(BaseCommand):
|
|||
pprint(connection.queries)
|
||||
raise
|
||||
|
||||
objects = [] # type: List[object]
|
||||
objects: List[object] = [] # type: ignore[annotation-unchecked]
|
||||
model_objects = {}
|
||||
|
||||
import ietf.name.models
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
|
||||
import functools
|
||||
from urllib.parse import quote as urlquote
|
||||
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.http import urlquote
|
||||
|
||||
|
||||
|
||||
def nomcom_private_key_required(view_func):
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
from django.conf.urls import url, include
|
||||
from django.urls import re_path, include
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', TemplateView.as_view(template_name='main.html')),
|
||||
url(r'^announcement/', include('ietf.secr.announcement.urls')),
|
||||
url(r'^areas/', include('ietf.secr.areas.urls')),
|
||||
url(r'^console/', include('ietf.secr.console.urls')),
|
||||
url(r'^meetings/', include('ietf.secr.meetings.urls')),
|
||||
url(r'^rolodex/', include('ietf.secr.rolodex.urls')),
|
||||
url(r'^sreq/', include('ietf.secr.sreq.urls')),
|
||||
url(r'^telechat/', include('ietf.secr.telechat.urls')),
|
||||
re_path(r'^$', TemplateView.as_view(template_name='main.html')),
|
||||
re_path(r'^announcement/', include('ietf.secr.announcement.urls')),
|
||||
re_path(r'^areas/', include('ietf.secr.areas.urls')),
|
||||
re_path(r'^console/', include('ietf.secr.console.urls')),
|
||||
re_path(r'^meetings/', include('ietf.secr.meetings.urls')),
|
||||
re_path(r'^rolodex/', include('ietf.secr.rolodex.urls')),
|
||||
re_path(r'^sreq/', include('ietf.secr.sreq.urls')),
|
||||
re_path(r'^telechat/', include('ietf.secr.telechat.urls')),
|
||||
]
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
# Copyright The IETF Trust 2013-2020, All Rights Reserved
|
||||
from functools import wraps
|
||||
from urllib.parse import quote as urlquote
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.utils.http import urlquote
|
||||
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.doc.models import Document
|
||||
|
|
|
@ -102,6 +102,11 @@ USE_I18N = False
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
# Default primary key field type to use for models that don’t have a field with primary_key=True.
|
||||
# In the future (relative to 4.2), the default will become 'django.db.models.BigAutoField.'
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
|
||||
|
||||
if SERVER_MODE == 'production':
|
||||
MEDIA_ROOT = '/a/www/www6s/lib/dt/media/'
|
||||
MEDIA_URL = 'https://www.ietf.org/lib/dt/media/'
|
||||
|
@ -428,7 +433,6 @@ INSTALLED_APPS = [
|
|||
'django_celery_beat',
|
||||
'corsheaders',
|
||||
'django_markup',
|
||||
'django_password_strength',
|
||||
'oidc_provider',
|
||||
'simple_history',
|
||||
'tastypie',
|
||||
|
@ -1119,7 +1123,6 @@ CHECKS_LIBRARY_PATCHES_TO_APPLY = [
|
|||
'patch/change-oidc-provider-field-sizes-228.patch',
|
||||
'patch/fix-oidc-access-token-post.patch',
|
||||
'patch/fix-jwkest-jwt-logging.patch',
|
||||
'patch/fix-django-password-strength-kwargs.patch',
|
||||
'patch/django-cookie-delete-with-all-settings.patch',
|
||||
'patch/tastypie-django22-fielderror-response.patch',
|
||||
]
|
||||
|
|
|
@ -1204,15 +1204,15 @@ class SubmitTests(BaseSubmitTestCase):
|
|||
|
||||
Unlike some other tests in this module, does not confirm draft if this would be required.
|
||||
"""
|
||||
orig_draft = DocumentFactory(
|
||||
orig_draft: Document = DocumentFactory( # type: ignore[annotation-unchecked]
|
||||
type_id='draft',
|
||||
group=GroupFactory(type_id=group_type) if group_type else None,
|
||||
stream_id=stream_type,
|
||||
) # type: Document
|
||||
)
|
||||
name = orig_draft.name
|
||||
group = orig_draft.group
|
||||
new_rev = '%02d' % (int(orig_draft.rev) + 1)
|
||||
author = PersonFactory() # type: Person
|
||||
author: Person = PersonFactory() # type: ignore[annotation-unchecked]
|
||||
DocumentAuthor.objects.create(person=author, document=orig_draft)
|
||||
orig_draft.docextresource_set.create(name_id='faq', value='https://faq.example.com/')
|
||||
orig_draft.docextresource_set.create(name_id='wiki', value='https://wiki.example.com', display_name='Test Wiki')
|
||||
|
@ -1982,7 +1982,7 @@ class SubmitTests(BaseSubmitTestCase):
|
|||
group = GroupFactory()
|
||||
# someone to be notified of resource suggestion when permission not granted
|
||||
RoleFactory(group=group, person=PersonFactory(), name_id='chair')
|
||||
submission = SubmissionFactory(state_id='grp-appr', group=group) # type: Submission
|
||||
submission: Submission = SubmissionFactory(state_id='grp-appr', group=group) # type: ignore[annotation-unchecked]
|
||||
SubmissionExtResourceFactory(submission=submission)
|
||||
|
||||
# use secretary user to ensure we have permission to approve
|
||||
|
@ -2000,7 +2000,7 @@ class SubmitTests(BaseSubmitTestCase):
|
|||
group = GroupFactory()
|
||||
# someone to be notified of resource suggestion when permission not granted
|
||||
RoleFactory(group=group, person=PersonFactory(), name_id='chair')
|
||||
submission = SubmissionFactory(state_id=state, group=group) # type: Submission
|
||||
submission: Submission = SubmissionFactory(state_id=state, group=group) # type: ignore[annotation-unchecked]
|
||||
SubmissionExtResourceFactory(submission=submission)
|
||||
|
||||
url = urlreverse(
|
||||
|
@ -2052,7 +2052,7 @@ class SubmitTests(BaseSubmitTestCase):
|
|||
|
||||
def test_forcepost_with_extresources(self):
|
||||
# state needs to be one that has 'posted' as a next state
|
||||
submission = SubmissionFactory(state_id='grp-appr') # type: Submission
|
||||
submission: Submission = SubmissionFactory(state_id='grp-appr') # type: ignore[annotation-unchecked]
|
||||
SubmissionExtResourceFactory(submission=submission)
|
||||
|
||||
url = urlreverse(
|
||||
|
|
|
@ -10,11 +10,11 @@ import re
|
|||
import requests
|
||||
|
||||
from email.utils import parsedate_to_datetime
|
||||
from urllib.parse import quote as urlquote
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import smart_bytes, force_str
|
||||
from django.utils.http import urlquote
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% ifequal ref.source.get_state.slug 'rfc' %}
|
||||
{% if ref.source.get_state.slug == 'rfc' %}
|
||||
{% with ref.source.std_level as lvl %}
|
||||
{% if lvl %}{{ lvl }}{% endif %}
|
||||
{% endwith %}
|
||||
|
@ -72,7 +72,7 @@
|
|||
{% with ref.source.intended_std_level as lvl %}
|
||||
{% if lvl %}{{ lvl }}{% endif %}
|
||||
{% endwith %}
|
||||
{% endifequal %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ ref.relationship.name }}</td>
|
||||
<td>{{ ref.is_downref|default:'' }}</td>
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
</a>
|
||||
</td>
|
||||
<td>
|
||||
{% ifequal ref.target.document.get_state.slug 'rfc' %}
|
||||
{% if ref.target.document.get_state.slug == 'rfc' %}
|
||||
{% with ref.target.document.std_level as lvl %}
|
||||
{% if lvl %}{{ lvl }}{% endif %}
|
||||
{% endwith %}
|
||||
|
@ -59,7 +59,7 @@
|
|||
{% with ref.target.document.intended_std_level as lvl %}
|
||||
{% if lvl %}{{ lvl }}{% endif %}
|
||||
{% endwith %}
|
||||
{% endifequal %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ ref.relationship.name }}</td>
|
||||
<td>{{ ref.is_downref|default:'' }}</td>
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
{% block title %}Change password{% endblock %}
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'ietf/js/zxcvbn.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/password_strength.js' %}"></script>
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
{% block title %}Complete account creation{% endblock %}
|
||||
{% block js %}
|
||||
{{ block.super }}
|
||||
<script src="{% static 'ietf/js/zxcvbn.js' %}"></script>
|
||||
<script src="{% static 'ietf/js/password_strength.js' %}"></script>
|
||||
{{ form.media.js }}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
# Copyright The IETF Trust 2007-2022, All Rights Reserved
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.conf.urls.static import static as static_url
|
||||
from django.contrib import admin
|
||||
from django.contrib.sitemaps import views as sitemap_views
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import include, path
|
||||
from django.views import static as static_view
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.defaults import server_error
|
||||
from django.urls import path
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
|
10
ietf/utils/http.py
Normal file
10
ietf/utils/http.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Copyright The IETF Trust 2023, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
def is_ajax(request):
|
||||
"""Checks whether a request was an AJAX call
|
||||
|
||||
See https://docs.djangoproject.com/en/3.1/releases/3.1/#id2 - this implements the
|
||||
exact reproduction of the deprecated method suggested there.
|
||||
"""
|
||||
return request.headers.get("x-requested-with") == "XMLHttpRequest"
|
|
@ -6,9 +6,9 @@ import debug # pyflakes:ignore
|
|||
|
||||
from inspect import isclass
|
||||
|
||||
from django.conf.urls import url as django_url
|
||||
from django.views.generic import View
|
||||
from django.urls import re_path
|
||||
from django.utils.encoding import force_str
|
||||
from django.views.generic import View
|
||||
|
||||
def url(regex, view, kwargs=None, name=None):
|
||||
if callable(view) and hasattr(view, '__name__'):
|
||||
|
@ -42,5 +42,5 @@ def url(regex, view, kwargs=None, name=None):
|
|||
#debug.show('branch')
|
||||
#debug.show('name')
|
||||
pass
|
||||
return django_url(regex, view, kwargs=kwargs, name=name)
|
||||
return re_path(regex, view, kwargs=kwargs, name=name)
|
||||
|
||||
|
|
|
@ -11,10 +11,11 @@ from urllib.parse import urlparse, urlsplit, urlunsplit
|
|||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import RegexValidator, URLValidator, EmailValidator, _lazy_re_compile, BaseValidator
|
||||
from django.core.validators import RegexValidator, URLValidator, EmailValidator, BaseValidator
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils.deconstruct import deconstructible
|
||||
from django.utils.ipv6 import is_valid_ipv6_address
|
||||
from django.utils.regex_helper import _lazy_re_compile # type: ignore
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -1,7 +1,5 @@
|
|||
[mypy]
|
||||
|
||||
#mypy_path = ./stubs/
|
||||
|
||||
ignore_missing_imports = True
|
||||
|
||||
plugins =
|
||||
|
@ -9,12 +7,3 @@ plugins =
|
|||
|
||||
[mypy.plugins.django-stubs]
|
||||
django_settings_module = ietf.settings
|
||||
|
||||
[mypy-ietf.group.migrations.0004_add_group_feature_fields]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-ietf.group.migrations.0002_groupfeatures_historicalgroupfeatures]
|
||||
ignore_errors = True
|
||||
|
||||
[mypy-ietf.doc.migrations.0001_initial]
|
||||
ignore_errors = True
|
||||
|
|
|
@ -281,7 +281,7 @@ diff -ur oidc_provider.orig/migrations/0021_refresh_token_not_unique.py oidc_pro
|
|||
diff -ur oidc_provider.orig/models.py oidc_provider/models.py
|
||||
--- oidc_provider.orig/models.py 2018-09-14 21:34:52.000000000 +0200
|
||||
+++ oidc_provider/models.py 2020-06-07 13:34:26.830716635 +0200
|
||||
@@ -67,8 +67,8 @@
|
||||
@@ -66,8 +66,8 @@
|
||||
verbose_name=_(u'Client Type'),
|
||||
help_text=_(u'<b>Confidential</b> clients are capable of maintaining the confidentiality'
|
||||
u' of their credentials. <b>Public</b> clients are incapable.'))
|
||||
|
@ -292,7 +292,7 @@ diff -ur oidc_provider.orig/models.py oidc_provider/models.py
|
|||
response_types = models.ManyToManyField(ResponseType)
|
||||
jwt_alg = models.CharField(
|
||||
max_length=10,
|
||||
@@ -78,15 +78,15 @@
|
||||
@@ -77,15 +77,15 @@
|
||||
help_text=_(u'Algorithm used to encode ID Tokens.'))
|
||||
date_created = models.DateField(auto_now_add=True, verbose_name=_(u'Date Created'))
|
||||
website_url = models.CharField(
|
||||
|
@ -311,7 +311,7 @@ diff -ur oidc_provider.orig/models.py oidc_provider/models.py
|
|||
logo = models.FileField(
|
||||
blank=True, default='', upload_to='oidc_provider/clients', verbose_name=_(u'Logo Image'))
|
||||
reuse_consent = models.BooleanField(
|
||||
@@ -186,12 +186,12 @@
|
||||
@@ -185,12 +185,12 @@
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_(u'User'), on_delete=models.CASCADE)
|
||||
|
@ -328,7 +328,7 @@ diff -ur oidc_provider.orig/models.py oidc_provider/models.py
|
|||
|
||||
class Meta:
|
||||
verbose_name = _(u'Authorization Code')
|
||||
@@ -205,8 +205,8 @@
|
||||
@@ -204,8 +204,8 @@
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, null=True, verbose_name=_(u'User'), on_delete=models.CASCADE)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
--- django/contrib/messages/storage/cookie.py.orig 2020-08-13 11:10:36.719177122 +0200
|
||||
+++ django/contrib/messages/storage/cookie.py 2020-08-13 11:45:23.503463150 +0200
|
||||
@@ -95,6 +95,8 @@
|
||||
@@ -108,6 +108,8 @@
|
||||
response.delete_cookie(
|
||||
self.cookie_name,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
|
@ -11,7 +11,7 @@
|
|||
|
||||
--- django/http/response.py.orig 2020-08-13 11:16:04.060627793 +0200
|
||||
+++ django/http/response.py 2020-08-13 11:54:03.482476973 +0200
|
||||
@@ -210,12 +210,18 @@
|
||||
@@ -243,12 +243,18 @@
|
||||
value = signing.get_cookie_signer(salt=key + salt).sign(value)
|
||||
return self.set_cookie(key, value, **kwargs)
|
||||
|
||||
|
@ -43,7 +43,7 @@
|
|||
|
||||
--- django/contrib/sessions/middleware.py.orig 2020-08-13 12:12:12.401898114 +0200
|
||||
+++ django/contrib/sessions/middleware.py 2020-08-13 12:14:52.690520659 +0200
|
||||
@@ -42,6 +42,8 @@
|
||||
@@ -40,6 +40,8 @@
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
--- django_password_strength/widgets.py.orig 2020-06-24 16:07:28.479533134 +0200
|
||||
+++ django_password_strength/widgets.py 2020-06-24 16:08:09.540714290 +0200
|
||||
@@ -8,7 +8,7 @@
|
||||
Form widget to show the user how strong his/her password is.
|
||||
"""
|
||||
|
||||
- def render(self, name, value, attrs=None):
|
||||
+ def render(self, name, value, **kwargs):
|
||||
strength_markup = """
|
||||
<div style="margin-top: 10px;">
|
||||
<div class="progress" style="margin-bottom: 10px;">
|
||||
@@ -30,7 +30,7 @@
|
||||
except KeyError:
|
||||
self.attrs['class'] = 'password_strength'
|
||||
|
||||
- return mark_safe( super(PasswordInput, self).render(name, value, attrs) + strength_markup )
|
||||
+ return mark_safe( super(PasswordInput, self).render(name, value, **kwargs) + strength_markup )
|
||||
|
||||
class Media:
|
||||
js = (
|
||||
@@ -48,7 +48,7 @@
|
||||
super(PasswordConfirmationInput, self).__init__(attrs, render_value)
|
||||
self.confirm_with=confirm_with
|
||||
|
||||
- def render(self, name, value, attrs=None):
|
||||
+ def render(self, name, value, **kwargs):
|
||||
if self.confirm_with:
|
||||
self.attrs['data-confirm-with'] = 'id_%s' % self.confirm_with
|
||||
|
||||
@@ -68,4 +68,4 @@
|
||||
except KeyError:
|
||||
self.attrs['class'] = 'password_confirmation'
|
||||
|
||||
- return mark_safe( super(PasswordInput, self).render(name, value, attrs) + confirmation_markup )
|
||||
+ return mark_safe( super(PasswordInput, self).render(name, value, **kwargs) + confirmation_markup )
|
||||
|
|
@ -11,12 +11,10 @@ diff -ur oidc_provider.orig/lib/utils/common.py oidc_provider/lib/utils/common.p
|
|||
|
||||
--- oidc_provider.orig/lib/utils/oauth2.py 2020-05-22 15:09:21.009044320 +0200
|
||||
+++ oidc_provider/lib/utils/oauth2.py 2020-06-05 17:05:23.271285858 +0200
|
||||
@@ -21,10 +21,14 @@
|
||||
"""
|
||||
@@ -22,9 +22,13 @@
|
||||
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
||||
|
||||
- if re.compile('^[Bb]earer\s{1}.+$').match(auth_header):
|
||||
+ if re.compile(r'^[Bb]earer\s{1}.+$').match(auth_header):
|
||||
if re.compile(r'^[Bb]earer\s{1}.+$').match(auth_header):
|
||||
access_token = auth_header.split()[1]
|
||||
- else:
|
||||
+ elif request.method == 'GET':
|
||||
|
@ -27,13 +25,4 @@ diff -ur oidc_provider.orig/lib/utils/common.py oidc_provider/lib/utils/common.p
|
|||
+ access_token = ''
|
||||
|
||||
return access_token
|
||||
|
||||
@@ -39,7 +43,7 @@
|
||||
"""
|
||||
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
|
||||
|
||||
- if re.compile('^Basic\s{1}.+$').match(auth_header):
|
||||
+ if re.compile(r'^Basic\s{1}.+$').match(auth_header):
|
||||
b64_user_pass = auth_header.split()[1]
|
||||
try:
|
||||
user_pass = b64decode(b64_user_pass).decode('utf-8').split(':')
|
||||
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
--- tastypie/resources.py.orig 2020-08-24 13:14:25.463166100 +0200
|
||||
+++ tastypie/resources.py 2020-08-24 13:15:55.133759224 +0200
|
||||
@@ -15,7 +15,7 @@
|
||||
@@ -12,7 +12,7 @@
|
||||
ObjectDoesNotExist, MultipleObjectsReturned, ValidationError, FieldDoesNotExist
|
||||
)
|
||||
from django.core.signals import got_request_exception
|
||||
-from django.core.exceptions import ImproperlyConfigured
|
||||
+from django.core.exceptions import ImproperlyConfigured, FieldError
|
||||
from django.db.models.fields.related import ForeignKey
|
||||
try:
|
||||
from django.contrib.gis.db.models.fields import GeometryField
|
||||
@@ -2207,6 +2207,8 @@
|
||||
from django.urls.conf import re_path
|
||||
from tastypie.utils.timezone import make_naive_utc
|
||||
@@ -2198,6 +2198,8 @@
|
||||
return self.authorized_read_list(objects, bundle)
|
||||
except ValueError:
|
||||
raise BadRequest("Invalid resource lookup data provided (mismatched type).")
|
||||
|
@ -20,7 +20,7 @@
|
|||
"""
|
||||
--- tastypie/paginator.py.orig 2020-08-25 15:24:46.391588425 +0200
|
||||
+++ tastypie/paginator.py 2020-08-25 15:24:53.591797122 +0200
|
||||
@@ -128,6 +128,8 @@
|
||||
@@ -124,6 +124,8 @@
|
||||
except (AttributeError, TypeError):
|
||||
# If it's not a QuerySet (or it's ilk), fallback to ``len``.
|
||||
return len(self.objects)
|
||||
|
|
|
@ -5,11 +5,13 @@ argon2-cffi>=21.3.0 # For the Argon2 password hasher option
|
|||
beautifulsoup4>=4.11.1 # Only used in tests
|
||||
bibtexparser>=1.2.0 # Only used in tests
|
||||
bleach>=6
|
||||
types-bleach>=6
|
||||
celery>=5.2.6
|
||||
coverage>=4.5.4,<5.0 # Coverage 5.x moves from a json database to SQLite. Moving to 5.x will require substantial rewrites in ietf.utils.test_runner and ietf.release.views
|
||||
decorator>=5.1.1
|
||||
types-decorator>=5.1.1
|
||||
defusedxml>=0.7.1 # for TastyPie when using xml; not a declared dependency
|
||||
Django<3.2
|
||||
Django<4
|
||||
django-analytical>=3.1.0
|
||||
django-bootstrap5>=21.3
|
||||
django-celery-beat>=2.3.0
|
||||
|
@ -17,17 +19,17 @@ django-csp>=3.7
|
|||
django-cors-headers>=3.11.0
|
||||
django-debug-toolbar>=3.2.4
|
||||
django-markup>=1.5 # Limited use - need to reconcile against direct use of markdown
|
||||
django-oidc-provider>=0.7,<0.8 # 0.8 dropped Django 2 support
|
||||
django-password-strength>=1.2.1
|
||||
django-oidc-provider>=0.8 # 0.8 dropped Django 2 support
|
||||
django-referrer-policy>=1.0
|
||||
django-simple-history>=3.0.0
|
||||
django-stubs==1.8.0 # The django-stubs version used determines the the mypy version indicated below
|
||||
django-tastypie==0.14.3 # Version must be locked in sync with version of Django
|
||||
django-stubs>=4.2.0 # The django-stubs version used determines the the mypy version indicated below
|
||||
django-tastypie>=0.14.5 # Version must be locked in sync with version of Django
|
||||
django-vite>=2.0.2
|
||||
django-webtest>=1.9.10 # Only used in tests
|
||||
django-widget-tweaks>=1.4.12
|
||||
djlint>=1.0.0 # To auto-indent templates via "djlint --profile django --reformat"
|
||||
docutils>=0.18.1 # Used only by dbtemplates for RestructuredText
|
||||
types-docutils>=0.18.1
|
||||
factory-boy>=3.2.1
|
||||
github3.py>=3.2.0
|
||||
gunicorn>=20.1.0
|
||||
|
@ -40,21 +42,25 @@ jwcrypto>=1.2 # for signed notifications - this is aspirational, and is not r
|
|||
logging_tree>=1.9 # Used only by the showloggers management command
|
||||
lxml>=4.8.0,<5
|
||||
markdown>=3.3.6
|
||||
types-markdown>=3.3.6
|
||||
mock>=4.0.3 # Used only by tests, of course
|
||||
mypy==0.812 # Version requirements determined by django-stubs.
|
||||
types-mock>=4.0.3
|
||||
mypy<1.3 # Version requirements determined by django-stubs.
|
||||
oic>=1.3 # Used only by tests
|
||||
Pillow>=9.1.0
|
||||
psycopg2<2.9
|
||||
psycopg2>=2.9.6
|
||||
pyang>=2.5.3
|
||||
pyflakes>=2.4.0
|
||||
pyopenssl>=22.0.0 # Used by urllib3.contrib, which is used by PyQuery but not marked as a dependency
|
||||
pyquery>=1.4.3
|
||||
python-dateutil>=2.8.2
|
||||
types-python-dateutil>=2.8.2
|
||||
python-magic==0.4.18 # Versions beyond the yanked .19 and .20 introduce form failures
|
||||
python-memcached>=1.59 # for django.core.cache.backends.memcached
|
||||
python-mimeparse>=1.6 # from TastyPie
|
||||
pytz==2022.2.1 # Pinned as changes need to be vetted for their effect on Meeting fields
|
||||
requests>=2.27.1
|
||||
types-requests>=2.27.1
|
||||
requests-mock>=1.9.3
|
||||
rfc2html>=2.0.3
|
||||
scout-apm>=2.24.2
|
||||
|
|
Loading…
Reference in a new issue