def handle(self, *args, **options):
        self._commit = options['commit']
        self._courses = {}
        self._session = SessionManagement()
        self._recorder = RemoteRecorderManagement()

        if options['stdin']:
            for line in sys.stdin:
                self._process_course(line.rstrip('\n'))
        else:
            for session in args:
                self._process_course(session)
Ejemplo n.º 2
0
def set_panopto_generic_folder(event):
    session_api = SessionManagement()
    id_string = "%s - %s" % (event['name'], event['space']['id'])
    folder_name = event['name']
    folder_external_id = panopto_generic_external_id(id_string)
    creators = []

    folders = session_api.getFoldersList(search_query=event['name'])
    if folders and len(folders) == 1:
        folder_name = folders[0].Name
        folder_external_id = folders[0].ExternalId
        creators = get_panopto_folder_creators(folders[0].Id)

    event['recording']['folder']['name'] = folder_name
    event['recording']['folder']['external_id'] = folder_external_id
    event['recording']['folder']['auth'] = {'creators': creators}
Ejemplo n.º 3
0
def set_panopto_generic_folder(event):
    session_api = SessionManagement()
    id_string = "%s - %s" % (event['name'], event['space']['id'])
    folder_name = event['name']
    folder_external_id = panopto_generic_external_id(id_string)
    creators = []

    folders = session_api.getFoldersList(search_query=event['name'])
    if folders and len(folders) == 1:
        folder_name = folders[0].Name
        folder_external_id = folders[0].ExternalId
        creators = get_panopto_folder_creators(folders[0].Id)

    event['recording']['folder']['name'] = folder_name
    event['recording']['folder']['external_id'] = folder_external_id
    event['recording']['folder']['auth'] = { 'creators': creators }
Ejemplo n.º 4
0
class Folder(RESTDispatch):
    def __init__(self):
        self._session = SessionManagement()
        self._access = AccessManagement()
        self._user = UserManagement()

    def GET(self, request, **kwargs):
        folder_id = kwargs.get('folder_id')
        if (folder_id):
            return self._get_folder_details(space_id)
        else:
            params = {}
            for q in request.GET:
                params[q] = request.GET.get(q)

            return self._list_folders(params)

    def _get_folder_details(self, space_id):
        return self.json_response('{}')

    def _list_folders(self, args):
        folders = []
        if 'search' in args:
            search = args['search'].strip()
            if len(search) > 3:
                for folder in self._session.getFoldersList(search_query=search):
                    creators = []
                    viewers = []
                    deets = self._access.getFolderAccessDetails(folder['Id'])
                    if deets.UsersWithCreatorAccess:
                        response = self._user.getUsers(deets.UsersWithCreatorAccess.guid)
                        if response and response.User:
                            for user in response.User:
                                match = re.match(r'^%s\\(.+)$' % (settings.PANOPTO_API_APP_ID), user.UserKey)
                                creators.append({
                                    'key': match.group(1) if match else user.UserKey,
                                    'id': user.UserId
                                })

                    if deets.UsersWithViewerAccess:
                        response = self._user.getUsers(deets.UsersWithCreatorAccess.guid)
                        if response and response.User:
                            for user in response.User:
                                match = re.match(r'^%s\\(.+)$' % (settings.PANOPTO_API_APP_ID), user.UserKey)
                                viewers.append({
                                    'key': match.group(1) if match else user.UserKey,
                                    'id': user.UserId
                                })

                    folders.append({
                        'name': folder.Name,
                        'id': folder.Id,
                        'auth' : {
                            'creators': creators,
                            'viewers': viewers
                        }
                    })

        return self.json_response(folders)
Ejemplo n.º 5
0
class SessionBroadcast(RESTDispatch):
    def __init__(self):
        self._session_api = SessionManagement()
        self._audit_log = logging.getLogger('audit')

    def GET(self, request, **kwargs):
        session_id = kwargs.get('session_id')
        if session_id:
            raw_session = self._session_api.getSessionsById([session_id])[0][0]
            broadcast = {
                'is_broadcast': raw_session['IsBroadcast'],
            }
        else:
            broadcast = {}

        return self.json_response(broadcast)

    def PUT(self, request, **kwargs):
        try:
            session_id = kwargs.get('session_id')
            data = json.loads(request.body)
            is_broadcast = self._valid_boolean(
                data.get("is_broadcast", False), 'bad broadcast flag')
            self._session_api.updateSessionIsBroadcast(session_id,
                                                       is_broadcast)
            self._audit_log.info('%s set %s broadcast to %s' % (
                request.user, session_id, is_broadcast))

            return self.json_response({
                'recording_id': session_id
            })
        except InvalidParamException as ex:
            return self.error_response(400, "%s" % ex)
        except Exception as ex:
            return self.error_response(500, "Unable to save session: %s" % ex)

    def _valid_boolean(self, v, errstr):
        if not (v is None or type(v) == bool):
            raise InvalidParamException(errstr)

        return v
    def handle(self, *args, **options):
        self._commit = options['commit']
        self._courses = {}
        self._session = SessionManagement()
        self._recorder = RemoteRecorderManagement()

        if options['stdin']:
            for line in sys.stdin:
                self._process_course(line.rstrip('\n'))
        else:
            for session in args:
                self._process_course(session)
Ejemplo n.º 7
0
class Folder(RESTDispatch):
    def __init__(self):
        self._session = SessionManagement()
        self._access = AccessManagement()
        self._user = UserManagement()

    def GET(self, request, **kwargs):
        folder_id = kwargs.get("folder_id")
        if folder_id:
            return self._get_folder_details(space_id)
        else:
            params = {}
            for q in request.GET:
                params[q] = request.GET.get(q)

            return self._list_folders(params)

    def _get_folder_details(self, space_id):
        return self.json_response("{}")

    def _list_folders(self, args):
        folders = []
        if "search" in args:
            search = args["search"].strip()
            if len(search) > 3:
                for folder in self._session.getFoldersList(search_query=search):
                    creators = []
                    viewers = []
                    deets = self._access.getFolderAccessDetails(folder["Id"])
                    if deets.UsersWithCreatorAccess:
                        response = self._user.getUsers(deets.UsersWithCreatorAccess.guid)
                        if response and response.User:
                            for user in response.User:
                                match = re.match(r"^%s\\(.+)$" % (settings.PANOPTO_API_APP_ID), user.UserKey)
                                creators.append({"key": match.group(1) if match else user.UserKey, "id": user.UserId})

                    if deets.UsersWithViewerAccess:
                        response = self._user.getUsers(deets.UsersWithCreatorAccess.guid)
                        if response and response.User:
                            for user in response.User:
                                match = re.match(r"^%s\\(.+)$" % (settings.PANOPTO_API_APP_ID), user.UserKey)
                                viewers.append({"key": match.group(1) if match else user.UserKey, "id": user.UserId})

                    folders.append(
                        {"name": folder.Name, "id": folder.Id, "auth": {"creators": creators, "viewers": viewers}}
                    )

        return self.json_response(folders)
class Command(BaseCommand):
    help = "Matchrecording session dates to SWS meeting times"

    option_list = BaseCommand.option_list + (
        make_option('--commit',
                    dest='commit',
                    action="store_true",
                    default=False,
                    help='Update Panopto recording with SWS meeting time'),
        make_option('--stdin',
                    dest='stdin',
                    action="store_true",
                    default=False,
                    help='get Panopto session external ids on standard input'),
    )

    def handle(self, *args, **options):
        self._commit = options['commit']
        self._courses = {}
        self._session = SessionManagement()
        self._recorder = RemoteRecorderManagement()

        if options['stdin']:
            for line in sys.stdin:
                self._process_course(line.rstrip('\n'))
        else:
            for session in args:
                self._process_course(session)

    def _process_course(self, session_id):
        # 2015-spring-PSYCH-202-A-2015-06-04
        course = re.match(
            r'^(20[0-9]{2})-(winter|spring|summer|autumn)'
            r'-([A-Z ]+)-([0-9]{3})-([A-Z][A-Z0-9]*)-2*', session_id)

        if course:
            label = "%s,%s,%s,%s/%s" % (course.group(1), course.group(2),
                                        course.group(3), course.group(4),
                                        course.group(5))

            if label not in self._courses:
                now = datetime.datetime.now(tz.tzlocal()).replace(
                    second=0, microsecond=0)
                section = get_section_by_label(
                    label, include_instructor_not_on_time_schedule=False)
                (start, end) = self._lecture_times(section)
                self._courses[label] = {
                    'start': start.split(':'),
                    'end': end.split(':')
                }

            offered = self._courses[label]
        else:
            print >> sys.stderr, "unrecognized session id: %s" % session_id
            return

        pan_session = self._session.getSessionsByExternalId([session_id])
        if 'Session' in pan_session and len(pan_session.Session) == 1:
            # broken-ass suds.
            fsuds = re.match(r'.*\<a\:StartTime\>([^<]+)\<\/a\:StartTime\>.*',
                             self._session._api.last_received().plain())
            if not fsuds:
                Exception('Untrustable time')

            pan_start = parser.parse(fsuds.group(1))
            pan_start_local = pan_start.astimezone(tz.tzlocal())
            sws_start_local = pan_start_local.replace(
                hour=int(offered['start'][0]), minute=int(offered['start'][1]))
            sws_end_local = pan_start_local.replace(
                hour=int(offered['end'][0]), minute=int(offered['end'][1]))

            schedule_delta = sws_start_local - pan_start_local

            duration_delta = (sws_end_local - sws_start_local).seconds - int(
                pan_session.Session[0].Duration)

            if schedule_delta or duration_delta:
                pan_start = (pan_start_local + schedule_delta).astimezone(
                    tz.tzutc())

                duration = pan_session.Session[0].Duration
                if duration_delta:
                    duration += duration_delta

                pan_end = pan_start + datetime.timedelta(0, duration)

                adjustment = [
                    session_id,
                    '(%s)' % pan_session.Session[0].Id,
                    '' if self._commit else 'WOULD', 'RESCHEDULE',
                    fsuds.group(1), 'TO',
                    pan_start.isoformat(), ':'
                ]

                if schedule_delta.days < 0:
                    adjustment.append("(-%s shift)" %
                                      (datetime.timedelta() - schedule_delta))
                else:
                    adjustment.append("(%s shift)" % schedule_delta)

                if duration_delta:
                    adjustment.append('AND DURATION')
                    adjustment.append("%s" % duration_delta)
                    adjustment.append('seconds')

                print >> sys.stderr, ' '.join(adjustment)

                if self._commit:
                    result = self._recorder.updateRecordingTime(
                        pan_session.Session[0].Id, pan_start.isoformat(),
                        pan_end.isoformat())
                    if not result:
                        print >> sys.stderr, "FAIL: null return value"
                    elif result.ConflictsExist:
                        print >> sys.stderr, "CONFLICT: %s" % (
                            result.ConflictingSessions[0][0].SessionName)
                    else:
                        print >> sys.stderr, "UPDATED %s" % (
                            result.SessionIDs[0][0])
            else:
                print >> sys.stderr, "%s: UNCHANGED" % (session_id)

        else:
            print >> sys.stderr, "unrecognized session id: %s" % session_id

    def _lecture_times(self, section):
        for meeting in section.meetings:
            if (meeting.meeting_type in ['lecture', 'quiz', 'seminar']
                    and meeting.start_time and meeting.end_time):
                return meeting.start_time, meeting.end_time

        Exception("no lecture times set")
Ejemplo n.º 9
0
 def __init__(self):
     self._session_api = SessionManagement()
     self._audit_log = logging.getLogger('audit')
Ejemplo n.º 10
0
 def __init__(self):
     self._session_api = SessionManagement()
     self._recorder_api = RemoteRecorderManagement()
     self._access_api = AccessManagement()
     self._user_api = UserManagement()
     self._audit_log = logging.getLogger('audit')
Ejemplo n.º 11
0
class Session(RESTDispatch):
    def __init__(self):
        self._session_api = SessionManagement()
        self._recorder_api = RemoteRecorderManagement()
        self._access_api = AccessManagement()
        self._user_api = UserManagement()
        self._audit_log = logging.getLogger('audit')

    def GET(self, request, **kwargs):
        session_id = kwargs.get('session_id')
        if session_id:
            raw_session = self._session_api.getSessionsById(
                [session_id])[0][0]
            raw_access = self._access_api.getSessionAccessDetails(
                session_id)
            start_utc = pytz.utc.localize(
                raw_session['StartTime']).astimezone(tz.tzutc())
            session = {
                'creator_id': raw_session['CreatorId'],
                'description': raw_session['Description'],
                'duration': raw_session['Duration'],
                'external_id': raw_session['ExternalId'],
                'folder_id': raw_session['FolderId'],
                'folder_name': raw_session['FolderName'],
                'folder_creators': [],
                'id': raw_session['Id'],
                'is_video_url': raw_session['IosVideoUrl'],
                'is_broadcast': raw_session['IsBroadcast'],
                'is_public': raw_access['IsPublic'],
                'is_downloadable': raw_session['IsDownloadable'],
                'name': raw_session['Name'],
                'remote_recorder_ids': raw_session['RemoteRecorderIds'].get(
                    'guid', None),
                'share_page_url': raw_session['SharePageUrl'],
                'start_time': start_utc.isoformat(),
                'state': raw_session['State'],
                'status_message': raw_session['StatusMessage'],
                'thumb_url': raw_session['ThumbUrl'],
                'viewer_url': raw_session['ViewerUrl'],
            }
        else:
            session = {}

        return self.json_response(session)

    def POST(self, request, **kwargs):
        try:
            new_session = self._validate_session(request.body)

            session = self._recorder_api.scheduleRecording(new_session.get('name'),
                                                           new_session.get('folder_id'),
                                                           new_session.get('is_broadcast'),
                                                           new_session.get('start_time'),
                                                           new_session.get('end_time'),
                                                           new_session.get('recorder_id'))
            if session.ConflictsExist:
                conflict = session.ConflictingSessions[0][0]
                start_time = conflict.StartTime
                end_time = conflict.EndTime
                content = {
                    'conflict_name': conflict.SessionName,
                    'conflict_start': start_time.isoformat(),
                    'conflict_end': end_time.isoformat()
                }
                return self.error_response(409, "Schedule Conflict Exists",
                                           content=content)

            session_id = session.SessionIDs[0][0]

            self._session_api.updateSessionExternalId(
                session_id, new_session.get('external_id'))

            if new_session.get('is_public'):
                self._access_api.updateSessionIsPublic(session_id, True)

            messages = []
            creators = new_session.get('folder_creators')
            if creators and type(creators) is list:
                messages = self._sync_creators(
                    new_session.get('folder_id'), creators)

            self._audit_log.info('%s scheduled %s for %s from %s to %s' % (
                request.user, new_session.get('external_id'),
                new_session.get('uwnetid'), new_session.get('start_time'),
                new_session.get('end_time')))

            return self.json_response({
                'recording_id': session_id,
                'messages': messages
            })
        except InvalidParamException as ex:
            return self.error_response(400, "%s" % ex)
        except Exception as ex:
            return self.error_response(500, "Unable to save session: %s" % ex)

    def PUT(self, request, **kwargs):
        try:
            session_update = self._validate_session(request.body)
            session = self._session_api.getSessionsById(
                session_update.get('recording_id'))[0][0]

            start_utc = session.StartTime.astimezone(pytz.utc)
            end_utc = start_utc + datetime.timedelta(
                seconds=int(session.Duration))

            session_update_start = self._valid_time(session_update.get('start_time'))
            session_update_end = self._valid_time(session_update.get('end_time'))

            if not (start_utc.isoformat() == session_update_start
                    and end_utc.isoformat() == session_update_end):
                self._recorder_api.updateRecordingTime(
                    session.Id, session_update_start, session_update_end)

            access = self._access_api.getSessionAccessDetails(session.Id)
            if access.IsPublic != session_update.get('is_public'):
                self._access_api.updateSessionIsPublic(
                    session.Id, session_update.get('is_public'))

            if session.IsBroadcast != session_update.get('is_broadcast'):
                self._session_api.updateSessionIsBroadcast(
                    session.Id, session_update.get('is_broadcast'))

            folder_name = session_update.get('folder_name')
            if session.FolderName != folder_name:
                self._session_api.moveSessions(
                    [session.Id], session_update.get('folder_id'))

            messages = []
            creators = session_update.get('folder_creators')
            if creators and type(creators) is list:
                messages = self._sync_creators(
                    session_update.get('folder_id'), creators)

            self._audit_log.info('%s modified %s for %s from %s to %s in %s' % (
                request.user, session_update.get('external_id'),
                session_update.get('uwnetid'), session_update.get('start_time'),
                session_update.get('end_time'), session_update.get('folder_name')))

            return self.json_response({
                'recording_id': session.Id,
                'messages': messages
            })
        except InvalidParamException as ex:
            return self.error_response(400, "%s" % ex)
        except Exception as ex:
            return self.error_response(500, "Unable to save session: %s" % ex)

    def DELETE(self, request, **kwargs):
        try:
            session_id = self._valid_recorder_id(kwargs.get('session_id'))
            # do not permit param tampering
            key = course_event_key(request.GET.get('uwnetid', ''),
                                   request.GET.get('name', ''),
                                   request.GET.get('eid', ''),
                                   request.GET.get('rid', ''))

            if key != request.GET.get("key", None):
                raise InvalidParamException('Invalid Client Key')

            self._session_api.deleteSessions([session_id])
            self._audit_log.info('%s deleted session %s' %
                                 (request.user, session_id))
            return self.json_response({
                'deleted_recording_id': session_id
            })
        except InvalidParamException as err:
            return self.error_response(400, "Invalid Parameter: %s" % err)

    def _valid_folder(self, name, external_id):
        try:
            folder_id = Validation().panopto_id(external_id)
            return folder_id
        except InvalidParamException:
            pass

        try:
            if external_id and len(external_id):
                folders = self._session_api.getAllFoldersByExternalId(
                    [external_id])
                if folders and len(folders) == 1 and len(folders[0]):
                    return folders[0][0].Id

            folders = self._session_api.getFoldersList(search_query=name)
            if folders and len(folders):
                for folder in folders:
                    if folder.Name == name:
                        folder_id = folder.Id
                        if external_id and len(external_id):
                            self._session_api.updateFolderExternalId(
                                folder_id, external_id)

                        return folder_id

            new_folder = self._session_api.addFolder(name)
            if not new_folder:
                raise InvalidParamException('Cannot add folder: %s' % name)

            new_folder_id = new_folder.Id

            if external_id and len(external_id):
                self._session_api.updateFolderExternalId(
                    new_folder_id, external_id)

            return new_folder_id
        except Exception as ex:
            raise InvalidParamException('Cannot add folder: %s' % ex)

    def _validate_session(self, request_body):
        session = {}
        data = json.loads(request_body)

        session['recording_id'] = data.get("recording_id", "")
        session['uwnetid'] = data.get("uwnetid", "")
        session['name'] = self._valid_recording_name(data.get("name", "").strip())
        session['external_id'] = self._valid_external_id(
            data.get("external_id", "").strip())
        session['recorder_id'] = self._valid_recorder_id(
            data.get("recorder_id", "").strip())
        session['folder_external_id'] = data.get(
            "folder_external_id", "").strip()

        session['session_id'] = data.get("session_id", "").strip()
        if len(session['session_id']):
            self._valid_external_id(session['session_id'])

        # do not permit param tamperings
        key = course_event_key(session['uwnetid'], session['name'],
                               session['external_id'], session['recorder_id'])
        if key != data.get("key", ''):
            raise InvalidParamException('Invalid Client Key')

        session['is_broadcast'] = self._valid_boolean(data.get("is_broadcast", False))
        session['is_public'] = self._valid_boolean(data.get("is_public", False))
        session['start_time'] = self._valid_time(data.get("start_time", "").strip())
        session['end_time'] = self._valid_time(data.get("end_time", "").strip())
        session['folder_name'] = data.get("folder_name", "").strip()
        session['folder_id'] = self._valid_folder(session['folder_name'],
                                                  session['folder_external_id'])
        session['folder_creators'] = data.get("creators", None)
        return session

    def _valid_external_id(self, external_id):
        if external_id and len(external_id):
            return external_id

        raise InvalidParamException('bad external_id')

    def _valid_recorder_id(self, recorder_id):
        if (recorder_id):
            return Validation().panopto_id(recorder_id)

        raise InvalidParamException('missing recorder id')

    def _valid_recording_name(self, name):
        if name and len(name):
            return name

        raise InvalidParamException('bad recording name')

    def _valid_boolean(self, is_broadcast):
        if not (is_broadcast is None or type(is_broadcast) == bool):
            raise InvalidParamException('bad broadcast flag')

        return is_broadcast

    def _valid_time(self, time):
        if time and len(time):
            return time

        raise InvalidParamException('bad time value')

    def _sync_creators(self, folder_id, folder_creators):
        messages = []
        new_creator_ids = []
        deleted_creator_ids = []
        current_creators = get_panopto_folder_creators(folder_id)
        for creator in folder_creators:
            if creator not in current_creators:
                try:
                    new_creator_ids.append(self._get_panopto_user_id(creator))
                except PanoptoUserException as ex:
                    messages.append('Invalid UWNetId %s' % creator)

        for creator in current_creators:
            if creator not in folder_creators:
                try:
                    deleted_creator_ids.append(self._get_panopto_user_id(creator))
                except PanoptoUserException as ex:
                    messages.append('Invalid UWNetId %s' % creator)

        if len(new_creator_ids):
            try:
                self._access_api.grantUsersAccessToFolder(
                    folder_id, new_creator_ids, 'Creator')
            except PanoptoAPIException as ex:
                match = re.match(r'.*Server raised fault: \'(.+)\'$', str(ex))
                messages.append('%s: %s' % (creator, match.group(1) if match else str(ex)))

        if len(deleted_creator_ids):
            try:
                self._access_api.revokeUsersAccessFromFolder(
                    folder_id, deleted_creator_ids, 'Creator')
            except PanoptoAPIException as ex:
                match = re.match(r'.*Server raised fault: \'(.+)\'$', str(ex))
                messages.append('%s: %s' % (creator, match.group(1) if match else str(ex)))

        return messages

    def _get_panopto_user_id(self, netid):
        key = "%s\%s" % (settings.PANOPTO_API_APP_ID, netid)
        user = self._user_api.getUserByKey(key)
        if not user or user['UserId'] == '00000000-0000-0000-0000-000000000000':
            raise PanoptoUserException('Unprovisioned UWNetId: %s' % (netid))

        return user['UserId']
Ejemplo n.º 12
0
 def __init__(self):
     self._session = SessionManagement()
     self._access = AccessManagement()
     self._user = UserManagement()
Ejemplo n.º 13
0
 def __init__(self):
     self._session = SessionManagement()
     self._access = AccessManagement()
     self._user = UserManagement()
Ejemplo n.º 14
0
def get_sessions_by_external_ids(external_ids):
    api = SessionManagement()
    sessions = api.getSessionsByExternalId(external_ids)
    return sessions.Session if (sessions and 'Session' in sessions and
                                len(sessions.Session)) else None
Ejemplo n.º 15
0
def get_sessions_by_session_ids(session_ids):
    api = SessionManagement()
    sessions = api.getSessionsById(session_ids)
    return sessions.Session if (sessions and 'Session' in sessions and
                                len(sessions.Session)) else None
class Command(BaseCommand):
    help = "Matchrecording session dates to SWS meeting times"

    option_list = BaseCommand.option_list + (
        make_option('--commit', dest='commit', action="store_true",
                    default=False,
                    help='Update Panopto recording with SWS meeting time'),
        make_option('--stdin', dest='stdin', action="store_true",
                    default=False,
                    help='get Panopto session external ids on standard input'),
    )

    def handle(self, *args, **options):
        self._commit = options['commit']
        self._courses = {}
        self._session = SessionManagement()
        self._recorder = RemoteRecorderManagement()

        if options['stdin']:
            for line in sys.stdin:
                self._process_course(line.rstrip('\n'))
        else:
            for session in args:
                self._process_course(session)

    def _process_course(self, session_id):
        # 2015-spring-PSYCH-202-A-2015-06-04
        course = re.match(r'^(20[0-9]{2})-(winter|spring|summer|autumn)'
                          r'-([A-Z ]+)-([0-9]{3})-([A-Z][A-Z0-9]*)-2*',
                          session_id)

        if course:
            label = "%s,%s,%s,%s/%s" % (
                course.group(1), course.group(2), course.group(3),
                course.group(4), course.group(5))

            if label not in self._courses:
                now = datetime.datetime.now(
                    tz.tzlocal()).replace(second=0, microsecond=0)
                section = get_section_by_label(
                    label, include_instructor_not_on_time_schedule=False)
                (start, end) = self._lecture_times(section)
                self._courses[label] = {
                    'start': start.split(':'),
                    'end': end.split(':')
                }

            offered = self._courses[label]
        else:
            print >> sys.stderr, "unrecognized session id: %s" % session_id
            return

        pan_session = self._session.getSessionsByExternalId([session_id])
        if 'Session' in pan_session and len(pan_session.Session) == 1:
            # broken-ass suds.
            fsuds = re.match(r'.*\<a\:StartTime\>([^<]+)\<\/a\:StartTime\>.*',
                             self._session._api.last_received().plain())
            if not fsuds:
                Exception('Untrustable time')

            pan_start = parser.parse(fsuds.group(1))
            pan_start_local = pan_start.astimezone(tz.tzlocal())
            sws_start_local = pan_start_local.replace(
                hour=int(offered['start'][0]),
                minute=int(offered['start'][1]))
            sws_end_local = pan_start_local.replace(
                hour=int(offered['end'][0]),
                minute=int(offered['end'][1]))

            schedule_delta = sws_start_local - pan_start_local

            duration_delta = (sws_end_local - sws_start_local).seconds - int(
                pan_session.Session[0].Duration)

            if schedule_delta or duration_delta:
                pan_start = (pan_start_local +
                             schedule_delta).astimezone(tz.tzutc())

                duration = pan_session.Session[0].Duration
                if duration_delta:
                    duration += duration_delta

                pan_end = pan_start + datetime.timedelta(0, duration)

                adjustment = [session_id, '(%s)' % pan_session.Session[0].Id,
                              '' if self._commit else 'WOULD', 'RESCHEDULE',
                              fsuds.group(1), 'TO',
                              pan_start.isoformat(), ':']

                if schedule_delta.days < 0:
                    adjustment.append("(-%s shift)" % (datetime.timedelta() -
                                                       schedule_delta))
                else:
                    adjustment.append("(%s shift)" % schedule_delta)

                if duration_delta:
                    adjustment.append('AND DURATION')
                    adjustment.append("%s" % duration_delta)
                    adjustment.append('seconds')

                print >> sys.stderr, ' '.join(adjustment)

                if self._commit:
                    result = self._recorder.updateRecordingTime(
                        pan_session.Session[0].Id,
                        pan_start.isoformat(),
                        pan_end.isoformat())
                    if not result:
                        print >> sys.stderr, "FAIL: null return value"
                    elif result.ConflictsExist:
                        print >> sys.stderr, "CONFLICT: %s" % (
                            result.ConflictingSessions[0][0].SessionName)
                    else:
                        print >> sys.stderr, "UPDATED %s" % (
                            result.SessionIDs[0][0])
            else:
                print >> sys.stderr, "%s: UNCHANGED" % (session_id)

        else:
            print >> sys.stderr, "unrecognized session id: %s" % session_id

    def _lecture_times(self, section):
        for meeting in section.meetings:
            if (meeting.meeting_type in ['lecture', 'quiz', 'seminar'] and
                    meeting.start_time and meeting.end_time):
                return meeting.start_time, meeting.end_time

        Exception("no lecture times set")