Merge remote-tracking branch 'origin/main' into personal/jennifer/7.45.1.dev0.bootstrap-merge
# Conflicts: # ietf/templates/meeting/session_buttons_include.html
This commit is contained in:
commit
686a9150b3
35
changelog
35
changelog
|
@ -1,3 +1,38 @@
|
|||
ietfdb (7.46.0) ietf; urgency=medium
|
||||
|
||||
** bugfixes, security improvements, performance improvements **
|
||||
|
||||
* Merged in [19946] from rjsparks@nostrum.com:
|
||||
Allow the secretariat to request many more sessions.
|
||||
|
||||
* Merged in [19947] from rjsparks@nostrum.com:
|
||||
Add link to onsite tool to agenda. Fixes #3550.
|
||||
|
||||
* Merged in [19948] from rjsparks@nostrum.com:
|
||||
Update link to handling ballot positions. Fixes #3208.
|
||||
|
||||
* Merged in [19949] and [19950] from rjsparks@nostrum.com:
|
||||
Use tempfiles while rebuilding group and doc alias files. Fixes #3521.
|
||||
|
||||
* Merged in [19952] from rjsparks@nostrum.com:
|
||||
Only keep the first and most recent yang validator SubmissionCheck for
|
||||
any given submission. Fixes #3542.
|
||||
|
||||
* Merged in [19954] from jennifer@painless-security.com:
|
||||
Refactor session overlap computation to treat overlapping sessions
|
||||
correctly.
|
||||
|
||||
* Merged in [19967] from rjsparks@nostrum.com:
|
||||
From Kesara Rathnayake: Expire password reset links on use, password
|
||||
change through other mechanics, login, or a short configurable time
|
||||
(initially one hour).
|
||||
|
||||
* Merged in [19969] from jennifer@painless-security.com:
|
||||
Use correct UTC time when creating Meetecho conferences. Fixes #3565.
|
||||
|
||||
-- Robert Sparks <rjsparks@nostrum.com> 24 Feb 2022 03:05:28 +0000
|
||||
|
||||
|
||||
ietfdb (7.45.0) ietf; urgency=medium
|
||||
|
||||
** MeetEcho interim request integration, bugfixes **
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
# Everyting below this line is OBE
|
||||
|
||||
/personal/rjs/7.45.1.dev0@19962 # Mangled commit
|
||||
/personal/rjs/7.39.1.dev1@19554 # Optimization wasn't measured correctly
|
||||
/personal/rjs/7.36.1.dev0@19318 # Folded this into r19336
|
||||
/personal/rjs/7.36.1.dev0@19302 # Handled this in an earlier merge
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
from . import checks # pyflakes:ignore
|
||||
|
||||
# Don't add patch number here:
|
||||
__version__ = "7.45.1.dev0"
|
||||
__version__ = "7.46.1.dev0"
|
||||
|
||||
# set this to ".p1", ".p2", etc. after patching
|
||||
__patch__ = ""
|
||||
|
||||
__date__ = "$Date$"
|
||||
|
||||
__rev__ = "$Rev$ (dev) Latest release: Rev. 19938 "
|
||||
__rev__ = "$Rev$ (dev) Latest release: Rev. 19974 "
|
||||
|
||||
__id__ = "$Id$"
|
||||
|
|
|
@ -8,7 +8,11 @@ import datetime
|
|||
import io
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import stat
|
||||
import time
|
||||
|
||||
from tempfile import mkstemp
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
@ -102,8 +106,13 @@ class Command(BaseCommand):
|
|||
date = time.strftime("%Y-%m-%d_%H:%M:%S")
|
||||
signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date)
|
||||
|
||||
afile = io.open(settings.DRAFT_ALIASES_PATH, "w")
|
||||
vfile = io.open(settings.DRAFT_VIRTUAL_PATH, "w")
|
||||
ahandle, aname = mkstemp()
|
||||
os.close(ahandle)
|
||||
afile = io.open(aname,"w")
|
||||
|
||||
vhandle, vname = mkstemp()
|
||||
os.close(vhandle)
|
||||
vfile = io.open(vname,"w")
|
||||
|
||||
afile.write(signature)
|
||||
vfile.write(signature)
|
||||
|
@ -160,4 +169,11 @@ class Command(BaseCommand):
|
|||
|
||||
afile.close()
|
||||
vfile.close()
|
||||
|
||||
os.chmod(aname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
|
||||
os.chmod(vname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
|
||||
|
||||
shutil.move(aname, settings.DRAFT_ALIASES_PATH)
|
||||
shutil.move(vname, settings.DRAFT_VIRTUAL_PATH)
|
||||
|
||||
|
|
@ -7,7 +7,11 @@
|
|||
import datetime
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import time
|
||||
|
||||
from tempfile import mkstemp
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
|
@ -40,8 +44,13 @@ class Command(BaseCommand):
|
|||
date = time.strftime("%Y-%m-%d_%H:%M:%S")
|
||||
signature = '# Generated by %s at %s\n' % (os.path.abspath(__file__), date)
|
||||
|
||||
afile = io.open(settings.GROUP_ALIASES_PATH, "w")
|
||||
vfile = io.open(settings.GROUP_VIRTUAL_PATH, "w")
|
||||
ahandle, aname = mkstemp()
|
||||
os.close(ahandle)
|
||||
afile = io.open(aname,"w")
|
||||
|
||||
vhandle, vname = mkstemp()
|
||||
os.close(vhandle)
|
||||
vfile = io.open(vname,"w")
|
||||
|
||||
afile.write(signature)
|
||||
vfile.write(signature)
|
||||
|
@ -86,4 +95,10 @@ class Command(BaseCommand):
|
|||
dump_sublist(afile, vfile, group.acronym+'-chairs', IETF_DOMAIN, settings.GROUP_VIRTUAL_DOMAIN, get_group_role_emails(group, ['chair', 'delegate']))
|
||||
|
||||
afile.close()
|
||||
vfile.close()
|
||||
vfile.close()
|
||||
|
||||
os.chmod(aname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
|
||||
os.chmod(vname, stat.S_IWUSR|stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH)
|
||||
|
||||
shutil.move(aname, settings.GROUP_ALIASES_PATH)
|
||||
shutil.move(vname, settings.GROUP_VIRTUAL_PATH)
|
||||
|
|
|
@ -373,9 +373,11 @@ class IetfAuthTests(TestCase):
|
|||
|
||||
def test_reset_password(self):
|
||||
url = urlreverse(ietf.ietfauth.views.password_reset)
|
||||
email = 'someone@example.com'
|
||||
password = 'foobar'
|
||||
|
||||
user = User.objects.create(username="someone@example.com", email="someone@example.com")
|
||||
user.set_password("forgotten")
|
||||
user = User.objects.create(username=email, email=email)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
p = Person.objects.create(name="Some One", ascii="Some One", user=user)
|
||||
Email.objects.create(address=user.username, person=p, origin=user.username)
|
||||
|
@ -414,6 +416,39 @@ class IetfAuthTests(TestCase):
|
|||
self.assertEqual(len(q("form .is-invalid")), 0)
|
||||
self.assertTrue(self.username_in_htpasswd_file(user.username))
|
||||
|
||||
# reuse reset url
|
||||
r = self.client.get(confirm_url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# login after reset request
|
||||
empty_outbox()
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
r = self.client.post(url, { 'username': user.username })
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
confirm_url = self.extract_confirm_url(outbox[-1])
|
||||
|
||||
r = self.client.post(urlreverse(ietf.ietfauth.views.login), {'username': email, 'password': password})
|
||||
|
||||
r = self.client.get(confirm_url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
# change password after reset request
|
||||
empty_outbox()
|
||||
|
||||
r = self.client.post(url, { 'username': user.username })
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
confirm_url = self.extract_confirm_url(outbox[-1])
|
||||
|
||||
user.set_password('newpassword')
|
||||
user.save()
|
||||
|
||||
r = self.client.get(confirm_url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_review_overview(self):
|
||||
review_req = ReviewRequestFactory()
|
||||
assignment = ReviewAssignmentFactory(review_request=review_req,reviewer=EmailFactory(person__user__username='reviewer'))
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
|
||||
import importlib
|
||||
|
||||
from datetime import date as Date
|
||||
from datetime import date as Date, datetime as DateTime
|
||||
# needed if we revert to higher barrier for account creation
|
||||
#from datetime import datetime as DateTime, timedelta as TimeDelta, date as Date
|
||||
from collections import defaultdict
|
||||
|
@ -418,7 +418,16 @@ def password_reset(request):
|
|||
if form.is_valid():
|
||||
username = form.cleaned_data['username']
|
||||
|
||||
auth = django.core.signing.dumps(username, salt="password_reset")
|
||||
data = { 'username': username }
|
||||
if User.objects.filter(username=username).exists():
|
||||
user = User.objects.get(username=username)
|
||||
data['password'] = user.password and user.password[-4:]
|
||||
if user.last_login:
|
||||
data['last_login'] = user.last_login.timestamp()
|
||||
else:
|
||||
data['last_login'] = None
|
||||
|
||||
auth = django.core.signing.dumps(data, salt="password_reset")
|
||||
|
||||
domain = Site.objects.get_current().domain
|
||||
subject = 'Confirm password reset at %s' % domain
|
||||
|
@ -429,7 +438,7 @@ def password_reset(request):
|
|||
'domain': domain,
|
||||
'auth': auth,
|
||||
'username': username,
|
||||
'expire': settings.DAYS_TO_EXPIRE_REGISTRATION_LINK,
|
||||
'expire': settings.MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK,
|
||||
})
|
||||
|
||||
success = True
|
||||
|
@ -443,11 +452,16 @@ def password_reset(request):
|
|||
|
||||
def confirm_password_reset(request, auth):
|
||||
try:
|
||||
username = django.core.signing.loads(auth, salt="password_reset", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
|
||||
data = django.core.signing.loads(auth, salt="password_reset", max_age=settings.MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK * 60)
|
||||
username = data['username']
|
||||
password = data['password']
|
||||
last_login = None
|
||||
if data['last_login']:
|
||||
last_login = DateTime.fromtimestamp(data['last_login'])
|
||||
except django.core.signing.BadSignature:
|
||||
raise Http404("Invalid or expired auth")
|
||||
|
||||
user = get_object_or_404(User, username=username)
|
||||
user = get_object_or_404(User, username=username, password__endswith=password, last_login=last_login)
|
||||
|
||||
success = False
|
||||
if request.method == 'POST':
|
||||
|
|
|
@ -1099,7 +1099,7 @@ def create_interim_session_conferences(sessions):
|
|||
confs = meetecho_manager.create(
|
||||
group=session.group,
|
||||
description=str(session),
|
||||
start_time=ts.time,
|
||||
start_time=ts.utc_start_time(),
|
||||
duration=ts.duration,
|
||||
)
|
||||
except Exception as err:
|
||||
|
|
|
@ -487,6 +487,9 @@ class Room(models.Model):
|
|||
def video_stream_url(self):
|
||||
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id in ['meetecho']]
|
||||
return urlresources[0].url if urlresources else None
|
||||
def onsite_tool_url(self):
|
||||
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id in ['meetecho_onsite']]
|
||||
return urlresources[0].url if urlresources else None
|
||||
def webex_url(self):
|
||||
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id in ['webex']]
|
||||
return urlresources[0].url if urlresources else None
|
||||
|
|
|
@ -456,8 +456,8 @@ class InterimTests(TestCase):
|
|||
def test_create_interim_session_conferences(self, mock):
|
||||
mock_conf_mgr = mock.return_value # "instance" seen by the internals
|
||||
sessions = [
|
||||
SessionFactory(meeting__type_id='interim', remote_instructions='junk'),
|
||||
SessionFactory(meeting__type_id='interim', remote_instructions=''),
|
||||
SessionFactory(meeting__type_id='interim', meeting__time_zone='america/halifax', remote_instructions='junk'),
|
||||
SessionFactory(meeting__type_id='interim', meeting__time_zone='asia/kuala_lumpur', remote_instructions=''),
|
||||
]
|
||||
timeslots = [
|
||||
session.official_timeslotassignment().timeslot for session in sessions
|
||||
|
@ -482,18 +482,18 @@ class InterimTests(TestCase):
|
|||
mock_conf_mgr.create.return_value = [
|
||||
Conference(
|
||||
manager=mock_conf_mgr, id=1, public_id='some-uuid', description='desc',
|
||||
start_time=timeslots[0].time, duration=timeslots[0].duration, url='fake-meetecho-url',
|
||||
start_time=timeslots[0].utc_start_time(), duration=timeslots[0].duration, url='fake-meetecho-url',
|
||||
deletion_token='please-delete-me',
|
||||
),
|
||||
]
|
||||
create_interim_session_conferences([sessions[0]])
|
||||
self.assertTrue(mock_conf_mgr.create.called)
|
||||
self.assertCountEqual(
|
||||
self.assertEqual(
|
||||
mock_conf_mgr.create.call_args[1],
|
||||
{
|
||||
'group': sessions[0].group,
|
||||
'description': str(sessions[0]),
|
||||
'start_time': timeslots[0].time,
|
||||
'start_time': timeslots[0].utc_start_time(),
|
||||
'duration': timeslots[0].duration,
|
||||
}
|
||||
)
|
||||
|
@ -507,30 +507,30 @@ class InterimTests(TestCase):
|
|||
mock_conf_mgr.create.side_effect = [
|
||||
[Conference(
|
||||
manager=mock_conf_mgr, id=1, public_id='some-uuid', description='desc',
|
||||
start_time=timeslots[0].time, duration=timeslots[0].duration, url='different-fake-meetecho-url',
|
||||
start_time=timeslots[0].utc_start_time(), duration=timeslots[0].duration, url='different-fake-meetecho-url',
|
||||
deletion_token='please-delete-me',
|
||||
)],
|
||||
[Conference(
|
||||
manager=mock_conf_mgr, id=2, public_id='another-uuid', description='desc',
|
||||
start_time=timeslots[1].time, duration=timeslots[1].duration, url='another-fake-meetecho-url',
|
||||
start_time=timeslots[1].utc_start_time(), duration=timeslots[1].duration, url='another-fake-meetecho-url',
|
||||
deletion_token='please-delete-me-too',
|
||||
)],
|
||||
]
|
||||
create_interim_session_conferences([sessions[0], sessions[1]])
|
||||
self.assertTrue(mock_conf_mgr.create.called)
|
||||
self.assertCountEqual(
|
||||
self.assertEqual(
|
||||
mock_conf_mgr.create.call_args_list,
|
||||
[
|
||||
({
|
||||
'group': sessions[0].group,
|
||||
'description': str(sessions[0]),
|
||||
'start_time': timeslots[0].time,
|
||||
'start_time': timeslots[0].utc_start_time(),
|
||||
'duration': timeslots[0].duration,
|
||||
},),
|
||||
({
|
||||
'group': sessions[1].group,
|
||||
'description': str(sessions[1]),
|
||||
'start_time': timeslots[1].time,
|
||||
'start_time': timeslots[1].utc_start_time(),
|
||||
'duration': timeslots[1].duration,
|
||||
},),
|
||||
]
|
||||
|
|
|
@ -90,13 +90,13 @@ class SessionForm(forms.Form):
|
|||
self.hidden = False
|
||||
|
||||
self.group = group
|
||||
formset_class = sessiondetailsformset_factory(max_num=3 if group.features.acts_like_wg else 12)
|
||||
formset_class = sessiondetailsformset_factory(max_num=3 if group.features.acts_like_wg else 50)
|
||||
self.session_forms = formset_class(group=self.group, meeting=meeting, data=data)
|
||||
super(SessionForm, self).__init__(data=data, *args, **kwargs)
|
||||
|
||||
# Allow additional sessions for non-wg-like groups
|
||||
if not self.group.features.acts_like_wg:
|
||||
self.fields['num_session'].choices = ((n, str(n)) for n in range(1, 13))
|
||||
self.fields['num_session'].choices = ((n, str(n)) for n in range(1, 51))
|
||||
|
||||
self.fields['comments'].widget = forms.Textarea(attrs={'rows':'3','cols':'65'})
|
||||
|
||||
|
|
|
@ -995,6 +995,7 @@ DE_GFM_BINARY = '/usr/bin/de-gfm.ruby2.5'
|
|||
|
||||
# Account settings
|
||||
DAYS_TO_EXPIRE_REGISTRATION_LINK = 3
|
||||
MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK = 60
|
||||
HTPASSWD_COMMAND = "/usr/bin/htpasswd"
|
||||
HTPASSWD_FILE = "/www/htpasswd"
|
||||
|
||||
|
|
|
@ -548,10 +548,43 @@ $(function () {
|
|||
|
||||
// hints for the current schedule
|
||||
|
||||
function updateSessionConstraintViolations() {
|
||||
// do a sweep on sessions sorted by start time
|
||||
let scheduledSessions = [];
|
||||
/** Find all pairs of overlapping intervals
|
||||
*
|
||||
* @param data Array of arbitrary interval-like objects with 'start' and 'end' properties
|
||||
* @returns Map from data item index to a list of overlapping data item indexes
|
||||
*/
|
||||
function findOverlappingIntervals(data) {
|
||||
const overlaps = {}; // results
|
||||
// Build ordered lists of start/end times, keeping track of the original index for each item
|
||||
const startIndexes = data.map((d, i) => ({time: d.start, index: i}));
|
||||
startIndexes.sort((a, b) => (b.time - a.time)); // sort reversed
|
||||
const endIndexes = data.map((d, i) => ({time: d.end, index: i}));
|
||||
endIndexes.sort((a, b) => (b.time - a.time)); // sort reversed
|
||||
|
||||
// items are sorted in reverse, so pop() will get the earliest item from each list
|
||||
let nextStart = startIndexes.pop();
|
||||
let nextEnd = endIndexes.pop();
|
||||
const openIntervalIndexes = [];
|
||||
while (nextStart && nextEnd) {
|
||||
if (nextStart.time < nextEnd.time) {
|
||||
// an interval opened - it overlaps all open intervals and all open intervals overlap it
|
||||
for (const intervalIndex of openIntervalIndexes) {
|
||||
overlaps[intervalIndex].push(nextStart.index);
|
||||
}
|
||||
overlaps[nextStart.index] = [...openIntervalIndexes]; // make a copy of the open list
|
||||
openIntervalIndexes.push(nextStart.index);
|
||||
nextStart = startIndexes.pop();
|
||||
} else {
|
||||
// an interval closed - remove its index from the list of open intervals
|
||||
openIntervalIndexes.splice(openIntervalIndexes.indexOf(nextEnd.index), 1);
|
||||
nextEnd = endIndexes.pop();
|
||||
}
|
||||
}
|
||||
return overlaps;
|
||||
}
|
||||
|
||||
function updateSessionConstraintViolations() {
|
||||
let scheduledSessions = [];
|
||||
sessions.each(function () {
|
||||
let timeslot = jQuery(this).closest(".timeslot");
|
||||
if (timeslot.length === 1) {
|
||||
|
@ -565,19 +598,8 @@ $(function () {
|
|||
}
|
||||
});
|
||||
|
||||
scheduledSessions.sort(function (a, b) {
|
||||
if (a.start < b.start) {
|
||||
return -1;
|
||||
}
|
||||
if (a.start > b.start) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
let currentlyOpen = {};
|
||||
let openedIndex = 0;
|
||||
let markSessionConstraintViolations = function (sess, currentlyOpen) {
|
||||
// helper function to mark constraint violations
|
||||
const markSessionConstraintViolations = function (sess, currentlyOpen) {
|
||||
sess.element.find(".constraints > span").each(function() {
|
||||
let sessionIds = this.dataset.sessions;
|
||||
|
||||
|
@ -596,25 +618,15 @@ $(function () {
|
|||
});
|
||||
};
|
||||
|
||||
for (let i = 0; i < scheduledSessions.length; ++i) {
|
||||
let s = scheduledSessions[i];
|
||||
|
||||
// prune
|
||||
for (let sessionIdStr in currentlyOpen) {
|
||||
if (currentlyOpen[sessionIdStr].end <= s.start) {
|
||||
delete currentlyOpen[sessionIdStr];
|
||||
}
|
||||
// now go through the sessions and mark constraint violations
|
||||
const overlaps = findOverlappingIntervals(scheduledSessions);
|
||||
for (const index in overlaps) {
|
||||
const currentlyOpen = {};
|
||||
for (const overlapIndex of overlaps[index]) {
|
||||
const otherSess = scheduledSessions[overlapIndex];
|
||||
currentlyOpen[otherSess.id] = otherSess;
|
||||
}
|
||||
|
||||
// expand
|
||||
while (openedIndex < scheduledSessions.length && scheduledSessions[openedIndex].start < s.end) {
|
||||
let toAdd = scheduledSessions[openedIndex];
|
||||
currentlyOpen[toAdd.id] = toAdd;
|
||||
++openedIndex;
|
||||
}
|
||||
|
||||
// check for violated constraints
|
||||
markSessionConstraintViolations(s, currentlyOpen);
|
||||
markSessionConstraintViolations(scheduledSessions[index], currentlyOpen);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
28
ietf/submit/management/commands/purge_yang_checks.py
Normal file
28
ietf/submit/management/commands/purge_yang_checks.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Copyright The IETF Trust 2022 All Rights Reserved
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from ietf.submit.models import Submission, SubmissionCheck
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = ("Remove all but the first and last yangchecks for each Submission")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
print("Identifying purgeable SubmissionChecks")
|
||||
keep = set()
|
||||
for submission in tqdm(Submission.objects.all()):
|
||||
qs = submission.checks.filter(checker="yang validation")
|
||||
if qs.count() == 0:
|
||||
continue
|
||||
qs = qs.order_by("time")
|
||||
keep.add(qs.first().pk)
|
||||
keep.add(qs.last().pk)
|
||||
keep.discard(None)
|
||||
print("Purging SubmissionChecks")
|
||||
print(
|
||||
SubmissionCheck.objects.filter(checker="yang validation")
|
||||
.exclude(pk__in=list(keep))
|
||||
.delete()
|
||||
)
|
|
@ -6,7 +6,7 @@ email addresses included in the To and CC lines. (Feel free to cut this
|
|||
introductory paragraph, however.)
|
||||
|
||||
{% if doc.type_id == "draft" and doc.stream_id != "irtf" %}
|
||||
Please refer to https://www.ietf.org/blog/handling-iesg-ballot-positions/
|
||||
Please refer to https://www.ietf.org/about/groups/iesg/statements/handling-ballot-positions/
|
||||
for more information about how to handle DISCUSS and COMMENT positions.
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -68,6 +68,13 @@
|
|||
<i class="bi bi-camera-video"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{# Onsite tool (meetecho_onsite) #}
|
||||
{% if timeslot.location.onsite_tool_url %}
|
||||
<a class=""
|
||||
href="{{timeslot.location.onsite_tool_url|format:session }}"
|
||||
title="Onsite tool"><span class="fa fa-fw fa-street-view"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{# Audio stream #}
|
||||
{% if timeslot.location.audio_stream_url %}
|
||||
<a class="btn btn-outline-primary"
|
||||
|
|
|
@ -5,7 +5,7 @@ Hello,
|
|||
|
||||
https://{{ domain }}{% url "ietf.ietfauth.views.confirm_password_reset" auth %}
|
||||
|
||||
This link will expire in {{ expire }} days.
|
||||
This link will expire in {{ expire }} minutes.
|
||||
|
||||
If you have not requested a password reset you can ignore this email, your
|
||||
credentials have been left untouched.
|
||||
|
|
|
@ -11,7 +11,7 @@ from django.core.management.base import BaseCommand
|
|||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.doc.models import Document, State, DocAlias
|
||||
from ietf.submit.models import Submission, SubmissionCheck
|
||||
from ietf.submit.models import Submission
|
||||
from ietf.submit.checkers import DraftYangChecker
|
||||
|
||||
|
||||
|
@ -55,7 +55,9 @@ class Command(BaseCommand):
|
|||
if new_res != old_res:
|
||||
if self.verbosity > 1:
|
||||
self.stdout.write(" Saving new yang checker results for %s-%s" % (draft.name, draft.rev))
|
||||
SubmissionCheck.objects.create(submission=submission, checker=checker.name, passed=passed,
|
||||
qs = submission.checks.filter(checker=checker.name).order_by('time')
|
||||
submission.checks.filter(checker=checker.name).exclude(pk=qs.first().pk).delete()
|
||||
submission.checks.create(submission=submission, checker=checker.name, passed=passed,
|
||||
message=message, errors=errors, warnings=warnings, items=items,
|
||||
symbol=checker.symbol)
|
||||
else:
|
||||
|
|
|
@ -15,11 +15,14 @@ import debug # pyflakes: ignore
|
|||
|
||||
from datetime import datetime, timedelta
|
||||
from json import JSONDecodeError
|
||||
from pytz import utc
|
||||
from typing import Dict, Sequence, Union
|
||||
from urllib.parse import urljoin
|
||||
|
||||
|
||||
class MeetechoAPI:
|
||||
timezone = utc
|
||||
|
||||
def __init__(self, api_base: str, client_id: str, client_secret: str, request_timeout=3.01):
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
|
@ -57,10 +60,10 @@ class MeetechoAPI:
|
|||
return None
|
||||
|
||||
def _deserialize_time(self, s: str) -> datetime:
|
||||
return datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
|
||||
return self.timezone.localize(datetime.strptime(s, '%Y-%m-%d %H:%M:%S'))
|
||||
|
||||
def _serialize_time(self, dt: datetime) -> str:
|
||||
return dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
return dt.astimezone(self.timezone).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def _deserialize_duration(self, minutes: int) -> timedelta:
|
||||
return timedelta(minutes=minutes)
|
||||
|
|
|
@ -4,6 +4,7 @@ import datetime
|
|||
import requests
|
||||
import requests_mock
|
||||
|
||||
from pytz import timezone, utc
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import urljoin
|
||||
|
||||
|
@ -91,7 +92,7 @@ class APITests(TestCase):
|
|||
api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET)
|
||||
api_response = api.schedule_meeting(
|
||||
wg_token='my-token',
|
||||
start_time=datetime.datetime(2021, 9, 14, 10, 0, 0),
|
||||
start_time=utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
|
||||
duration=datetime.timedelta(minutes=130),
|
||||
description='interim-2021-wgname-01',
|
||||
extrainfo='message for staff',
|
||||
|
@ -117,23 +118,32 @@ class APITests(TestCase):
|
|||
},
|
||||
'Incorrect request content'
|
||||
)
|
||||
self.assertEqual(
|
||||
api_response,
|
||||
{
|
||||
'rooms': {
|
||||
'3d55bce0-535e-4ba8-bb8e-734911cf3c32': {
|
||||
'room': {
|
||||
'id': 18,
|
||||
'start_time': datetime.datetime(2021, 9, 14, 10, 0, 0),
|
||||
'duration': datetime.timedelta(minutes=130),
|
||||
'description': 'interim-2021-wgname-01',
|
||||
# same time in different time zones
|
||||
for start_time in [
|
||||
utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
|
||||
timezone('america/halifax').localize(datetime.datetime(2021, 9, 14, 7, 0, 0)),
|
||||
timezone('europe/kiev').localize(datetime.datetime(2021, 9, 14, 13, 0, 0)),
|
||||
timezone('pacific/easter').localize(datetime.datetime(2021, 9, 14, 5, 0, 0)),
|
||||
timezone('africa/porto-novo').localize(datetime.datetime(2021, 9, 14, 11, 0, 0)),
|
||||
]:
|
||||
self.assertEqual(
|
||||
api_response,
|
||||
{
|
||||
'rooms': {
|
||||
'3d55bce0-535e-4ba8-bb8e-734911cf3c32': {
|
||||
'room': {
|
||||
'id': 18,
|
||||
'start_time': start_time,
|
||||
'duration': datetime.timedelta(minutes=130),
|
||||
'description': 'interim-2021-wgname-01',
|
||||
},
|
||||
'url': 'https://meetings.conf.meetecho.com/interim/?short=3d55bce0-535e-4ba8-bb8e-734911cf3c32',
|
||||
'deletion_token': 'session-deletion-token',
|
||||
},
|
||||
'url': 'https://meetings.conf.meetecho.com/interim/?short=3d55bce0-535e-4ba8-bb8e-734911cf3c32',
|
||||
'deletion_token': 'session-deletion-token',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
f'Incorrect time conversion for {start_time.tzinfo.zone}',
|
||||
)
|
||||
|
||||
def test_fetch_meetings(self):
|
||||
self.maxDiff = 2048
|
||||
|
@ -181,7 +191,7 @@ class APITests(TestCase):
|
|||
'3d55bce0-535e-4ba8-bb8e-734911cf3c32': {
|
||||
'room': {
|
||||
'id': 18,
|
||||
'start_time': datetime.datetime(2021, 9, 14, 10, 0, 0),
|
||||
'start_time': utc.localize(datetime.datetime(2021, 9, 14, 10, 0, 0)),
|
||||
'duration': datetime.timedelta(minutes=130),
|
||||
'description': 'interim-2021-wgname-01',
|
||||
},
|
||||
|
@ -191,7 +201,7 @@ class APITests(TestCase):
|
|||
'e68e96d4-d38f-475b-9073-ecab46ca96a5': {
|
||||
'room': {
|
||||
'id': 23,
|
||||
'start_time': datetime.datetime(2021, 9, 15, 14, 30, 0),
|
||||
'start_time': utc.localize(datetime.datetime(2021, 9, 15, 14, 30, 0)),
|
||||
'duration': datetime.timedelta(minutes=30),
|
||||
'description': 'interim-2021-wgname-02',
|
||||
},
|
||||
|
@ -239,7 +249,7 @@ class APITests(TestCase):
|
|||
|
||||
def test_time_serialization(self):
|
||||
"""Time de/serialization should be consistent"""
|
||||
time = datetime.datetime.now().replace(microsecond=0) # cut off to 0 microseconds
|
||||
time = datetime.datetime.now(utc).replace(microsecond=0) # cut off to 0 microseconds
|
||||
api = MeetechoAPI(API_BASE, CLIENT_ID, CLIENT_SECRET)
|
||||
self.assertEqual(api._deserialize_time(api._serialize_time(time)), time)
|
||||
|
||||
|
@ -253,7 +263,7 @@ class ConferenceManagerTests(TestCase):
|
|||
'session-1-uuid': {
|
||||
'room': {
|
||||
'id': 1,
|
||||
'start_time': datetime.datetime(2022,2,4,1,2,3),
|
||||
'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)),
|
||||
'duration': datetime.timedelta(minutes=45),
|
||||
'description': 'some-description',
|
||||
},
|
||||
|
@ -263,7 +273,7 @@ class ConferenceManagerTests(TestCase):
|
|||
'session-2-uuid': {
|
||||
'room': {
|
||||
'id': 2,
|
||||
'start_time': datetime.datetime(2022,2,5,4,5,6),
|
||||
'start_time': utc.localize(datetime.datetime(2022,2,5,4,5,6)),
|
||||
'duration': datetime.timedelta(minutes=90),
|
||||
'description': 'another-description',
|
||||
},
|
||||
|
@ -280,7 +290,7 @@ class ConferenceManagerTests(TestCase):
|
|||
id=1,
|
||||
public_id='session-1-uuid',
|
||||
description='some-description',
|
||||
start_time=datetime.datetime(2022,2,4,1,2,3),
|
||||
start_time=utc.localize(datetime.datetime(2022, 2, 4, 1, 2, 3)),
|
||||
duration=datetime.timedelta(minutes=45),
|
||||
url='https://example.com/some/url',
|
||||
deletion_token='delete-me',
|
||||
|
@ -290,7 +300,7 @@ class ConferenceManagerTests(TestCase):
|
|||
id=2,
|
||||
public_id='session-2-uuid',
|
||||
description='another-description',
|
||||
start_time=datetime.datetime(2022,2,5,4,5,6),
|
||||
start_time=utc.localize(datetime.datetime(2022, 2, 5, 4, 5, 6)),
|
||||
duration=datetime.timedelta(minutes=90),
|
||||
url='https://example.com/another/url',
|
||||
deletion_token='delete-me-too',
|
||||
|
@ -306,7 +316,7 @@ class ConferenceManagerTests(TestCase):
|
|||
'session-1-uuid': {
|
||||
'room': {
|
||||
'id': 1,
|
||||
'start_time': datetime.datetime(2022,2,4,1,2,3),
|
||||
'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)),
|
||||
'duration': datetime.timedelta(minutes=45),
|
||||
'description': 'some-description',
|
||||
},
|
||||
|
@ -325,7 +335,7 @@ class ConferenceManagerTests(TestCase):
|
|||
id=1,
|
||||
public_id='session-1-uuid',
|
||||
description='some-description',
|
||||
start_time=datetime.datetime(2022,2,4,1,2,3),
|
||||
start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)),
|
||||
duration=datetime.timedelta(minutes=45),
|
||||
url='https://example.com/some/url',
|
||||
deletion_token='delete-me',
|
||||
|
@ -341,7 +351,7 @@ class ConferenceManagerTests(TestCase):
|
|||
'session-1-uuid': {
|
||||
'room': {
|
||||
'id': 1,
|
||||
'start_time': datetime.datetime(2022,2,4,1,2,3),
|
||||
'start_time': utc.localize(datetime.datetime(2022,2,4,1,2,3)),
|
||||
'duration': datetime.timedelta(minutes=45),
|
||||
'description': 'some-description',
|
||||
},
|
||||
|
@ -359,7 +369,7 @@ class ConferenceManagerTests(TestCase):
|
|||
id=1,
|
||||
public_id='session-1-uuid',
|
||||
description='some-description',
|
||||
start_time=datetime.datetime(2022,2,4,1,2,3),
|
||||
start_time=utc.localize(datetime.datetime(2022,2,4,1,2,3)),
|
||||
duration=datetime.timedelta(minutes=45),
|
||||
url='https://example.com/some/url',
|
||||
deletion_token='delete-me',
|
||||
|
|
Binary file not shown.
Loading…
Reference in a new issue