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:
Jennifer Richards 2022-03-04 15:14:52 -04:00
commit 686a9150b3
21 changed files with 277 additions and 95 deletions

View file

@ -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 **

View file

@ -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

View file

@ -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$"

View file

@ -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)

View file

@ -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)

View file

@ -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'))

View file

@ -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':

View file

@ -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:

View file

@ -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

View file

@ -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,
},),
]

View file

@ -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'})

View file

@ -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"

View file

@ -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);
}
}

View 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()
)

View file

@ -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 %}

View file

@ -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"

View file

@ -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.

View file

@ -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:

View file

@ -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)

View file

@ -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.