Skip to content
This repository was archived by the owner on May 28, 2022. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3e58cde

Browse files
author
Stephen Romansky
committedDec 19, 2014
Add database.py tests and refactoring
- Remove test methods that only check function return types - Create resources folder for common test data like test rss feeds - Fix database.py query bugs - Add database folder for database related tests - Refactor common database fixtures into the database conftest file - Refactor comments in database.py - Refactor queries in database.py to have better style and sanitization - Add comments and examples of the presentation time stamps - Remove unused functions from database.py - Add httpretty mock tests for database.py - Add a database schema upgrade test - Add tests that check multiple scenarios of each method in database.py - Replace string % operators with calls to format for nonlog string formatting - Remove several try/finally statements and replaced them with 'with's - Fix an exception logging statement which referred to an out of scope value - Add failure and presentation equality and inequality comparison functions - Add example of parameterized test with fixtures - Add fixtures based on summer camp 2010 and 2011 stored data Related #484 Related #667 Related #670
1 parent 145f7f9 commit 3e58cde

File tree

16 files changed

+1031
-319
lines changed

16 files changed

+1031
-319
lines changed
 

‎dev_requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ httpretty
44
mock
55
pep8==1.4.6
66
pytest-cov==1.6
7+
pytest-httpretty==0.2.0
78
pytest==2.5.2
89
Sphinx<=1.2.3

‎src/freeseer/framework/database.py

+220-178
Large diffs are not rendered by default.

‎src/freeseer/framework/failure.py

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def __init__(self, talkID, comment, indicator, release=False):
3939
self.indicator = indicator
4040
self.release = release
4141

42+
def __eq__(self, obj):
43+
return self.__dict__ == obj.__dict__
44+
45+
def __ne__(self, obj):
46+
return not self == obj
47+
4248

4349
class Report():
4450
def __init__(self, presentation, failure):

‎src/freeseer/framework/presentation.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,28 @@
2424

2525
from os import path
2626

27+
from PyQt4.QtCore import QDate
28+
from PyQt4.QtCore import QDateTime
2729

30+
31+
# TODO: Add a print presentation method? print self.__dict__
32+
# TODO: Write a database record to Presentation method
2833
class Presentation(object):
2934
'''
3035
This class is responsible for encapsulate data about presentations
3136
and its database related operations
3237
'''
33-
def __init__(self, title, speaker="", description="", category="", event="Default", room="Default", date="", startTime="", endTime=""):
38+
39+
# TODO: Are Default variable necessary if they are all empty-string values?
40+
DEFAULT_ROOM = ''
41+
DEFAULT_DATE = ''
42+
DEFAULT_TIME = ''
43+
44+
# TODO: Confirm Presentation.date is or is not a QDate object.
45+
# TODO: Confirm Presentation.startTime should store a QDateTime object or a QTime object.
46+
# TODO: Confirm Presentation.endTime should store a QDateTime object or a QTime object.
47+
def __init__(self, title, speaker='', description='', category='', event='', room='',
48+
date='', startTime='', endTime=''):
3449
'''
3550
Initialize a presentation instance
3651
'''
@@ -44,6 +59,23 @@ def __init__(self, title, speaker="", description="", category="", event="Defaul
4459
self.startTime = startTime
4560
self.endTime = endTime
4661

62+
if not self.room:
63+
self.room = self.DEFAULT_ROOM
64+
65+
# Set the date, startTime, or endTime if they are null timestamp values
66+
if self.date == QDate():
67+
self.date = self.DEFAULT_DATE
68+
if self.startTime == QDateTime():
69+
self.startTime = self.DEFAULT_TIME
70+
if self.endTime == QDateTime():
71+
self.endTime = self.DEFAULT_TIME
72+
73+
def __eq__(self, obj):
74+
return self.__dict__ == obj.__dict__
75+
76+
def __ne__(self, obj):
77+
return not self == obj
78+
4779

4880
class PresentationFile(Presentation):
4981

‎src/freeseer/frontend/configtool/configtool.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -750,7 +750,7 @@ def show_plugin_widget_dialog(self, widget, name):
750750
self.dialog.closeButton = QtGui.QPushButton("Close")
751751
self.dialog_layout.addWidget(self.dialog.closeButton)
752752
self.connect(self.dialog.closeButton, QtCore.SIGNAL('clicked()'), self.dialog.close)
753-
self.dialog.setWindowTitle(u'{} Setup'.format(name))
753+
self.dialog.setWindowTitle('{} Setup'.format(name))
754754
self.dialog.setModal(True)
755755
self.dialog.show()
756756

‎src/freeseer/frontend/record/record.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -428,8 +428,8 @@ def record(self):
428428
if self.current_event and self.current_room:
429429
starttime = QtCore.QDateTime().currentDateTime()
430430
stoptime = starttime.addSecs(900)
431-
talkid = self.db.get_talk_between_time(self.current_event, self.current_room,
432-
starttime.toString(), stoptime.toString())
431+
talkid = self.db.get_talk_between_dates(self.current_event, self.current_room,
432+
starttime.toString(), stoptime.toString())
433433

434434
if talkid is not None:
435435
for i in range(self.mainWidget.talkComboBox.count()):
@@ -638,8 +638,8 @@ def show_report_widget(self):
638638
self.reportWidget.speakerLabel2.setText(p.speaker)
639639
self.reportWidget.eventLabel2.setText(p.event)
640640
self.reportWidget.roomLabel2.setText(p.room)
641-
self.reportWidget.startTimeLabel2.setText(p.startTime)
642-
self.reportWidget.endTimeLabel2.setText(p.endTime)
641+
self.reportWidget.startTimeLabel2.setText(p.startTime.time().toString())
642+
self.reportWidget.endTimeLabel2.setText(p.endTime.time().toString())
643643

644644
# Get existing report if there is one.
645645
talk_id = self.current_presentation_id()

‎src/freeseer/frontend/reporteditor/reporteditor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ def add_talk(self):
170170
"", # level
171171
unicode(self.addTalkWidget.eventLineEdit.text()),
172172
unicode(self.addTalkWidget.roomLineEdit.text()),
173-
unicode(datetime.toString()),
173+
datetime.date(),
174174
unicode(self.addTalkWidget.endTimeEdit.text()))
175175

176176
# Do not add talks if they are empty strings

‎src/freeseer/frontend/talkeditor/talkeditor.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ def retranslate(self):
254254

255255
def load_presentations_model(self):
256256
# Load Presentation Model
257+
# FIXME: The raw databse values are being loaded into the view. This means the date, startTime, and
258+
# endTime are showing the raw QDate(Time) values. There should be a layer that converts between the
259+
# frontend values and the backend.
257260
self.presentationModel = self.db.get_presentations_model()
258261
self.proxy = QSortFilterProxyModel()
259262
self.proxy.setSourceModel(self.presentationModel)
@@ -395,9 +398,6 @@ def update_talk(self):
395398

396399
def create_presentation(self, talkDetailsWidget):
397400
"""Creates and returns an instance of Presentation using data from the input fields"""
398-
date = talkDetailsWidget.dateEdit.date()
399-
startTime = talkDetailsWidget.startTimeEdit.time()
400-
endTime = talkDetailsWidget.endTimeEdit.time()
401401

402402
title = unicode(talkDetailsWidget.titleLineEdit.text()).strip()
403403
if title:
@@ -408,9 +408,9 @@ def create_presentation(self, talkDetailsWidget):
408408
unicode(talkDetailsWidget.categoryLineEdit.text()).strip(),
409409
unicode(talkDetailsWidget.eventLineEdit.text()).strip(),
410410
unicode(talkDetailsWidget.roomLineEdit.text()).strip(),
411-
unicode(date.toString(Qt.ISODate)),
412-
unicode(startTime.toString(Qt.ISODate)),
413-
unicode(endTime.toString(Qt.ISODate)))
411+
talkDetailsWidget.dateEdit.date(),
412+
talkDetailsWidget.startTimeEdit.time().toString('hh:mm ap'),
413+
talkDetailsWidget.endTimeEdit.time().toString('hh:mm ap'))
414414

415415
def show_new_talk_popup(self):
416416
"""Displays a modal dialog with a talk details view

‎src/freeseer/plugins/importer/csv_importer.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,13 @@ def get_presentations(self, fname):
6363
'Level': unicode(row.get('Level', ''), 'utf-8'),
6464
'Event': unicode(row.get('Event', ''), 'utf-8'),
6565
'Room': unicode(row.get('Room', ''), 'utf-8'),
66-
'Time': unicode(row.get('Time', ''), 'utf-8')
66+
'Time': unicode(row.get('Time', ''), 'utf-8'),
67+
'Problem': unicode(row.get('Problem', ''), 'utf-8'), # optional value from report csv
68+
'Error': unicode(row.get('Error', ''), 'utf-8') # optional value from report csv
6769
}
68-
6970
presentations.append(talk)
7071

7172
except IOError:
72-
log.exception("CSV: File %s not found", csv_file)
73+
log.exception("CSV: File %s not found", fname)
7374

7475
return presentations

‎src/freeseer/tests/framework/database/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# freeseer - vga/presentation capture software
5+
#
6+
# Copyright (C) 2014 Free and Open Source Software Learning Centre
7+
# http://fosslc.org
8+
#
9+
# This program is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License as published by
11+
# the Free Software Foundation, either version 3 of the License, or
12+
# (at your option) any later version.
13+
#
14+
# This program is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+
# For support, questions, suggestions or any other inquiries, visit:
23+
# http://wiki.github.com/Freeseer/freeseer/
24+
25+
import pytest
26+
from PyQt4 import QtCore
27+
28+
from freeseer.framework.config.profile import Profile
29+
from freeseer.framework.presentation import Presentation
30+
31+
32+
@pytest.fixture
33+
def db(tmpdir):
34+
"""Return a new empty QtDBConnector"""
35+
profile = Profile(str(tmpdir), 'testing')
36+
return profile.get_database()
37+
38+
39+
@pytest.fixture
40+
def fake_presentation():
41+
"""Returns a presentation object with fake data. Also demonstrates how to construct time values for presentations"""
42+
today = QtCore.QDate().currentDate() # today
43+
# TODO: The currentDateTime object sets milliseconds. Convert it back to a string then to a QDateTime object again
44+
# to get rid of milliseconds. There has got to be a better solution...
45+
current_time = QtCore.QDateTime.fromString(QtCore.QDateTime().currentDateTime().toString()) # yyyy-mm-ddThh:mm:ss
46+
return Presentation(
47+
title='MITM presentation attacks',
48+
speaker='Alice and Eve',
49+
description='Field survey of current MITM presentation attacks.',
50+
category='Security category',
51+
event='testing event',
52+
room='1300',
53+
date=today,
54+
startTime=current_time.addSecs(60 * 5),
55+
endTime=current_time.addSecs(60 * 10)
56+
)
57+
58+
59+
@pytest.fixture
60+
def presentation_sc2010():
61+
"""Presentation object from the Summercamp2010 rss feed."""
62+
return Presentation(
63+
title='Managing map data in a database',
64+
speaker='Andrew Ross',
65+
description='''This talk will provide a brief introduction to geospatial technologies. It will focus on '''
66+
'''managing map data with a relational database. Managing map data with a database provides the atomicity, '''
67+
'''security, access that is difficult to achieve otherwise. It also provides powerful techniques for querying'''
68+
''' spatial aware data which can enable new insights.''',
69+
category='Intermediate',
70+
event='Summercamp2010',
71+
room='Rom AB113',
72+
date=QtCore.QDate.fromString('2010-05-14T10:45', QtCore.Qt.ISODate),
73+
startTime=QtCore.QDateTime.fromString('2010-05-14T10:45', 'yyyy-MM-ddThh:mm')
74+
)
75+
76+
77+
@pytest.fixture
78+
def presentation_sc2011_maxwell_rss():
79+
"""Creates a presentation from the summercamp2011 csv file"""
80+
return Presentation(
81+
title='Building NetBSD',
82+
speaker='David Maxwell',
83+
description='''People who are interested in learning about operating systems have a lot of topics to absorb,'''
84+
''' but the very first barrier that gets in people's way is that you need to be able to build the software. '''
85+
'''If you can't build it, you can't make changes. If building it is painful, you'll find other things to do '''
86+
'''with your time.\n'''
87+
'''\tThe NetBSD Project has a build system that goes far beyond what many other projects implement. Come to '''
88+
'''this talk about learn about\n'''
89+
'''\tbuild.sh and the features available that make multi-architecture and embedded development environments '''
90+
'''a breeze with NetBSD.\n'''
91+
'''\tNetBSD website: http://www.NetBSD.org/''',
92+
event='SC2011',
93+
category='Beginner',
94+
room='',
95+
date=QtCore.QDate.fromString('2011-08-17T20:29', QtCore.Qt.ISODate),
96+
startTime=QtCore.QDateTime.fromString('2011-08-17T20:29', 'yyyy-MM-ddThh:mm')
97+
)
98+
99+
100+
@pytest.fixture
101+
def presentation_sc2011_maxwell_csv():
102+
"""Creates a presentation from the summercamp2011 csv file"""
103+
return Presentation(
104+
title='Building NetBSD',
105+
speaker='David Maxwell',
106+
description='''People who are interested in learning about operating systems have a lot of topics to absorb,'''
107+
''' but the very first barrier that gets in people's way is that you need to be able to build the software. '''
108+
'''If you can't build it, you can't make changes. If building it is painful, you'll find other things to do '''
109+
'''with your time.\n'''
110+
'''\tThe NetBSD Project has a build system that goes far beyond what many other projects implement. Come to '''
111+
'''this talk about learn about\n'''
112+
'''\tbuild.sh and the features available that make multi-architecture and embedded development environments '''
113+
'''a breeze with NetBSD.\n'''
114+
'''\tNetBSD website: http://www.NetBSD.org/''',
115+
event='SC2011',
116+
category='Beginner',
117+
room='None',
118+
date=QtCore.QDate.fromString('2011-08-17T20:29', QtCore.Qt.ISODate),
119+
startTime=QtCore.QDateTime.fromString('2011-08-17T20:29', 'yyyy-MM-ddThh:mm').time().toString('hh:mm ap')
120+
)
121+
122+
123+
@pytest.fixture
124+
def presentation_sc2011_dixon():
125+
"""Presentation from the Summercamp2011 csv file"""
126+
return Presentation(
127+
title='Lecture Broadcast and Capture using BigBlueButton',
128+
speaker='Fred Dixon',
129+
description='''BigBlueButton is an open source web conferencing system for distance education. It's goal is '''
130+
'''to enable remote students to have a high-quality learning experience. The #1 requested feature we've had '''
131+
'''over the last year is to integrate record and playback of a session.\n'''
132+
'''\n'''
133+
'''\t\n'''
134+
'''\tFred Dixon and Richard Alam, two of the BigBlueButton committers, will describe the architecture and '''
135+
'''implementation of record and playback as well as demonstrate the integration with Moodle to show how an '''
136+
'''educational institution can use BigBlueButton to setup virtual classrooms, record lectures, and provide '''
137+
'''students access to the recorded content from within the Moodle interface.\n'''
138+
'''\n'''
139+
'''\tWe will also demonstrate an prototype integration with popcorn.js (Mozilla project) using it as a '''
140+
'''playback client for the recorded content.''',
141+
event='SC2011',
142+
category='Intermediate',
143+
room='',
144+
date='',
145+
startTime=''
146+
)

‎src/freeseer/tests/framework/database/test_database.py

+428
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# freeseer - vga/presentation capture software
5+
#
6+
# Copyright (C) 2014 Free and Open Source Software Learning Centre
7+
# http://fosslc.org
8+
#
9+
# This program is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License as published by
11+
# the Free Software Foundation, either version 3 of the License, or
12+
# (at your option) any later version.
13+
#
14+
# This program is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+
# For support, questions, suggestions or any other inquiries, visit:
23+
# http://wiki.github.com/Freeseer/freeseer/
24+
import pytest
25+
26+
from freeseer.framework.failure import Failure
27+
from freeseer.framework.presentation import Presentation
28+
29+
30+
@pytest.fixture
31+
def failure1():
32+
return Failure(
33+
talkID='1',
34+
comment='Fake presentation',
35+
indicator='It is a fixture',
36+
release=True
37+
)
38+
39+
40+
@pytest.fixture
41+
def failure2():
42+
return Failure(
43+
talkID='2',
44+
comment='Non-existant failure',
45+
indicator='This is a fake failure',
46+
release=True
47+
)
48+
49+
50+
def test_insert_failure(db, failure1):
51+
"""Assert that a failure can be inserted in the database failure table"""
52+
db.insert_failure(failure1)
53+
assert db.get_report('1') == failure1
54+
55+
56+
def test_get_reports(db, fake_presentation, failure1, failure2):
57+
"""Assert that failure reports may be fetched from the database"""
58+
db.insert_presentation(fake_presentation)
59+
db.insert_failure(failure1)
60+
db.insert_failure(failure2) # There is no presentation associated with failure2.talkId
61+
62+
reports = db.get_reports()
63+
64+
assert len(reports) == 2
65+
assert reports[0].presentation.title == fake_presentation.title
66+
assert reports[0].failure == failure1
67+
assert not reports[1].presentation
68+
assert reports[1].failure == failure2
69+
70+
71+
def test_export_reports_to_csv(db, tmpdir, failure1, failure2):
72+
"""Assert that failure reports from the database can exported to a csv file"""
73+
fake_presentation = Presentation(title='Fake it',
74+
speaker='John Doe',
75+
room='200')
76+
presentation_sc2010 = Presentation(title='A fake presentation',
77+
speaker='No one',
78+
room='Mystery')
79+
80+
temp_csv = str(tmpdir.join('reports.csv'))
81+
82+
db.insert_presentation(fake_presentation)
83+
db.insert_presentation(presentation_sc2010)
84+
db.insert_failure(failure1)
85+
db.insert_failure(failure2)
86+
db.export_reports_to_csv(temp_csv)
87+
88+
expected_csv_lines = [
89+
'Title,Speaker,Abstract,Category,Event,Room,Date,StartTime,EndTime,Problem,Error\r\n',
90+
'Fake it,John Doe,,,Default,200,{},{},{},It is a fixture,Fake presentation\r\n'.format(
91+
Presentation.DEFAULT_DATE, Presentation.DEFAULT_TIME, Presentation.DEFAULT_TIME),
92+
'A fake presentation,No one,,,Default,Mystery,{},{},{},This is a fake failure,Non-existant failure\r\n'.format(
93+
Presentation.DEFAULT_DATE, Presentation.DEFAULT_TIME, Presentation.DEFAULT_TIME)
94+
]
95+
96+
with open(temp_csv) as fd:
97+
assert fd.readlines() == expected_csv_lines
98+
99+
100+
def test_delete_failure(db, failure1):
101+
"""Assert that failure reports can be deleted, without side effects, from the database"""
102+
db.insert_failure(failure1)
103+
db.delete_failure('1')
104+
assert not db.get_report('1')
105+
106+
107+
def test_update_failure(db, failure1):
108+
"""Assert that a given failure can be updated without causing side effects, in the database"""
109+
failure_update = Failure(talkID='1',
110+
comment='Super fake presentation',
111+
indicator='It is not really real',
112+
release=True)
113+
114+
db.insert_failure(failure1) # make sure that failure1 is actually in the database
115+
assert db.get_report('1') == failure1
116+
db.update_failure('1', failure_update) # replace failure1 with failure2
117+
assert db.get_report('1') == failure_update
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# freeseer - vga/presentation capture software
5+
#
6+
# Copyright (C) 2014 Free and Open Source Software Learning Centre
7+
# http://fosslc.org
8+
#
9+
# This program is free software: you can redistribute it and/or modify
10+
# it under the terms of the GNU General Public License as published by
11+
# the Free Software Foundation, either version 3 of the License, or
12+
# (at your option) any later version.
13+
#
14+
# This program is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+
# For support, questions, suggestions or any other inquiries, visit:
23+
# http://wiki.github.com/Freeseer/freeseer/
24+
25+
import os
26+
27+
import httpretty
28+
import pytest
29+
30+
from freeseer.tests.framework.database.conftest import presentation_sc2010
31+
from freeseer.tests.framework.database.conftest import presentation_sc2011_dixon
32+
from freeseer.tests.framework.database.conftest import presentation_sc2011_maxwell_rss
33+
from freeseer.framework.presentation import Presentation
34+
35+
rss_resource_relative_path = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, 'resources', 'sample_rss_data')
36+
feed_2010 = (os.path.join(rss_resource_relative_path, 'summercamp2010.rss'),
37+
'http://fosslc.org/drupal/presentations_rss/summercamp2010')
38+
feed_2011 = (os.path.join(rss_resource_relative_path, 'sc2011.rss'),
39+
'http://fosslc.org/drupal/presentations/sc2011')
40+
41+
42+
@pytest.mark.httpretty
43+
@pytest.mark.parametrize("feed, expected", [
44+
(feed_2010, [(presentation_sc2010(), True)]),
45+
(feed_2011, [
46+
(presentation_sc2010(), False),
47+
(presentation_sc2011_maxwell_rss(), True),
48+
(presentation_sc2011_dixon(), True)
49+
]),
50+
(feed_2011, [(Presentation('fake title'), False)])
51+
])
52+
def test_add_talks_from_rss(db, feed, expected):
53+
"""Assert that presentations can be added from an rss feed"""
54+
feed_filename, feed_url = feed
55+
56+
with open(feed_filename) as presentation_rss_file:
57+
feed_data = presentation_rss_file.read()
58+
59+
# Monkey patch GET request.
60+
httpretty.register_uri(httpretty.GET, feed_url, body=feed_data, content_type='application/rss+xml')
61+
db.add_talks_from_rss(feed_url)
62+
63+
for presentation, expectation in expected:
64+
assert db._helper_presentation_exists(presentation) == expectation

‎src/freeseer/tests/framework/test_database.py

-124
This file was deleted.

‎src/freeseer/tests/frontend/test_cli_talk.py

-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def test_add_talk(self, mock_profile):
6464
cli.parse_args(self.parser, args)
6565
talks = self.db.get_talks()
6666
talks.next() # Point to talk data
67-
talks.next() # Skip default blank entry
6867
talks.next() # Skip first test entry
6968
talks.next() # Skip second test entry
7069
record = talks.record()

0 commit comments

Comments
 (0)
This repository has been archived.