Skip to content

Commit 3a76e4a

Browse files
committed
Added an API endpoint for bluesheet information upload. Factored out code in common with manual upload_session_bluesheets(). Tweaked ietf.secr.proceedings.utils.handle_upload_file() to handle temp files in addition to http file objects, to support the API endpoint.
- Legacy-Id: 18067
1 parent 729b52f commit 3a76e4a

4 files changed

Lines changed: 207 additions & 42 deletions

File tree

ietf/api/tests.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# -*- coding: utf-8 -*-
33

44

5+
import json
56
import os
67
import sys
78

@@ -21,7 +22,7 @@
2122
from ietf.group.factories import RoleFactory
2223
from ietf.meeting.factories import MeetingFactory, SessionFactory
2324
from ietf.meeting.test_data import make_meeting_test_data
24-
from ietf.person.factories import PersonFactory
25+
from ietf.person.factories import PersonFactory, random_faker
2526
from ietf.person.models import PersonalApiKey
2627
from ietf.stats.models import MeetingRegistration
2728
from ietf.utils.mail import outbox, get_payload_text
@@ -135,6 +136,93 @@ def test_api_set_session_video_url(self):
135136
event = doc.latest_event()
136137
self.assertEqual(event.by, recman)
137138

139+
def test_api_upload_bluesheet(self):
140+
url = urlreverse('ietf.meeting.views.api_upload_bluesheet')
141+
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
142+
recman = recmanrole.person
143+
meeting = MeetingFactory(type_id='ietf')
144+
session = SessionFactory(group__type_id='wg', meeting=meeting)
145+
group = session.group
146+
apikey = PersonalApiKey.objects.create(endpoint=url, person=recman)
147+
148+
people = [
149+
{"name":"Andrea Andreotti", "affiliation": "Azienda"},
150+
{"name":"Bosse Bernadotte", "affiliation": "Bolag"},
151+
{"name":"Charles Charlemagne", "affiliation": "Compagnie"},
152+
]
153+
for i in range(3):
154+
faker = random_faker()
155+
people.append(dict(name=faker.name(), affiliation=faker.company()))
156+
bluesheet = json.dumps(people)
157+
158+
# error cases
159+
r = self.client.post(url, {})
160+
self.assertContains(r, "Missing apikey parameter", status_code=400)
161+
162+
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
163+
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
164+
badrole.person.user.last_login = timezone.now()
165+
badrole.person.user.save()
166+
r = self.client.post(url, {'apikey': badapikey.hash()} )
167+
self.assertContains(r, "Restricted to roles Recording Manager, Secretariat", status_code=403)
168+
169+
r = self.client.post(url, {'apikey': apikey.hash()} )
170+
self.assertContains(r, "Too long since last regular login", status_code=400)
171+
recman.user.last_login = timezone.now()
172+
recman.user.save()
173+
174+
r = self.client.get(url, {'apikey': apikey.hash()} )
175+
self.assertContains(r, "Method not allowed", status_code=405)
176+
177+
r = self.client.post(url, {'apikey': apikey.hash()} )
178+
self.assertContains(r, "Missing meeting parameter", status_code=400)
179+
180+
181+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, } )
182+
self.assertContains(r, "Missing group parameter", status_code=400)
183+
184+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym} )
185+
self.assertContains(r, "Missing item parameter", status_code=400)
186+
187+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym, 'item': '1'} )
188+
self.assertContains(r, "Missing bluesheet parameter", status_code=400)
189+
190+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': '1', 'group': group.acronym,
191+
'item': '1', 'bluesheet': bluesheet, })
192+
self.assertContains(r, "No sessions found for meeting", status_code=400)
193+
194+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': 'bogous',
195+
'item': '1', 'bluesheet': bluesheet, })
196+
self.assertContains(r, "No sessions found in meeting '%s' for group 'bogous'"%meeting.number, status_code=400)
197+
198+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
199+
'item': '1', 'bluesheet': "foobar", })
200+
self.assertContains(r, "Invalid json value: 'foobar'", status_code=400)
201+
202+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
203+
'item': '5', 'bluesheet': bluesheet, })
204+
self.assertContains(r, "No item '5' found in list of sessions for group", status_code=400)
205+
206+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
207+
'item': 'foo', 'bluesheet': bluesheet, })
208+
self.assertContains(r, "Expected a numeric value for 'item', found 'foo'", status_code=400)
209+
210+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
211+
'item': '1', 'bluesheet': bluesheet, })
212+
self.assertContains(r, "Done", status_code=200)
213+
214+
r = self.client.post(url, {'apikey': apikey.hash(), 'meeting': meeting.number, 'group': group.acronym,
215+
'item': '1', 'bluesheet': bluesheet, })
216+
debug.show('bluesheet')
217+
self.assertContains(r, "Done", status_code=200)
218+
219+
bluesheet = session.sessionpresentation_set.filter(document__type__slug='bluesheets').first().document
220+
with open(bluesheet.get_file_name()) as file:
221+
text = file.read()
222+
for p in people:
223+
self.assertIn(p['name'], text)
224+
self.assertIn(p['affiliation'], text)
225+
138226
def test_person_export(self):
139227
person = PersonFactory()
140228
url = urlreverse('ietf.api.views.PersonalInformationExportView')

ietf/api/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
# Let Meetecho trigger recording imports
3030
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
3131
# Let the registration system notify us about registrations
32-
url(r'^notify/meeting/registration', api_views.api_new_meeting_registration),
32+
url(r'^notify/meeting/bluesheet/?$', meeting_views.api_upload_bluesheet),
33+
# Let the registration system notify us about registrations
34+
url(r'^notify/meeting/registration/?', api_views.api_new_meeting_registration),
3335
# OpenID authentication provider
3436
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
3537
# For meetecho access

ietf/meeting/views.py

Lines changed: 110 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pytz
1414
import re
1515
import tarfile
16+
import tempfile
1617
import markdown2
1718

1819

@@ -1702,55 +1703,27 @@ def upload_session_bluesheets(request, session_id, num):
17021703
if len(sessions) > 1:
17031704
session_number = 1 + sessions.index(session)
17041705

1705-
bluesheet_sp = session.sessionpresentation_set.filter(document__type='bluesheets').first()
1706-
17071706
if request.method == 'POST':
17081707
form = UploadBlueSheetForm(request.POST,request.FILES)
17091708
if form.is_valid():
17101709
file = request.FILES['file']
1711-
_, ext = os.path.splitext(file.name)
1712-
if bluesheet_sp:
1713-
doc = bluesheet_sp.document
1714-
doc.rev = '%02d' % (int(doc.rev)+1)
1715-
bluesheet_sp.rev = doc.rev
1716-
bluesheet_sp.save()
1717-
else:
1718-
ota = session.official_timeslotassignment()
1719-
sess_time = ota and ota.timeslot.time
1720-
if not sess_time:
1721-
return HttpResponse("Cannot receive uploads for an unscheduled session. Please check the session ID.", status=410, content_type="text/plain")
1722-
if session.meeting.type_id=='ietf':
1723-
name = 'bluesheets-%s-%s-%s' % (session.meeting.number,
1724-
session.group.acronym,
1725-
sess_time.strftime("%Y%m%d%H%M"))
1726-
title = 'Bluesheets IETF%s: %s : %s' % (session.meeting.number,
1727-
session.group.acronym,
1728-
sess_time.strftime("%a %H:%M"))
1729-
else:
1730-
name = 'bluesheets-%s-%s' % (session.meeting.number, sess_time.strftime("%Y%m%d%H%M"))
1731-
title = 'Bluesheets %s: %s' % (session.meeting.number, sess_time.strftime("%a %H:%M"))
1732-
doc = Document.objects.create(
1733-
name = name,
1734-
type_id = 'bluesheets',
1735-
title = title,
1736-
group = session.group,
1737-
rev = '00',
1738-
)
1739-
doc.states.add(State.objects.get(type_id='bluesheets',slug='active'))
1740-
DocAlias.objects.create(name=doc.name).docs.add(doc)
1741-
session.sessionpresentation_set.create(document=doc,rev='00')
1742-
filename = '%s-%s%s'% ( doc.name, doc.rev, ext)
1743-
doc.uploaded_filename = filename
1744-
e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev)
1745-
save_error = handle_upload_file(file, filename, session.meeting, 'bluesheets', request=request, encoding=form.file_encoding[file.name])
1710+
1711+
ota = session.official_timeslotassignment()
1712+
sess_time = ota and ota.timeslot.time
1713+
if not sess_time:
1714+
return HttpResponse("Cannot receive uploads for an unscheduled session. Please check the session ID.", status=410, content_type="text/plain")
1715+
1716+
1717+
save_error = save_bluesheet(request, session, file, encoding=form.file_encoding[file.name])
17461718
if save_error:
17471719
form.add_error(None, save_error)
17481720
else:
1749-
doc.save_with_history([e])
17501721
return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym)
17511722
else:
17521723
form = UploadBlueSheetForm()
17531724

1725+
bluesheet_sp = session.sessionpresentation_set.filter(document__type='bluesheets').first()
1726+
17541727
return render(request, "meeting/upload_session_bluesheets.html",
17551728
{'session': session,
17561729
'session_number': session_number,
@@ -1759,6 +1732,47 @@ def upload_session_bluesheets(request, session_id, num):
17591732
})
17601733

17611734

1735+
def save_bluesheet(request, session, file, encoding='utf-8'):
1736+
bluesheet_sp = session.sessionpresentation_set.filter(document__type='bluesheets').first()
1737+
_, ext = os.path.splitext(file.name)
1738+
1739+
if bluesheet_sp:
1740+
doc = bluesheet_sp.document
1741+
doc.rev = '%02d' % (int(doc.rev)+1)
1742+
bluesheet_sp.rev = doc.rev
1743+
bluesheet_sp.save()
1744+
else:
1745+
ota = session.official_timeslotassignment()
1746+
sess_time = ota and ota.timeslot.time
1747+
1748+
if session.meeting.type_id=='ietf':
1749+
name = 'bluesheets-%s-%s-%s' % (session.meeting.number,
1750+
session.group.acronym,
1751+
sess_time.strftime("%Y%m%d%H%M"))
1752+
title = 'Bluesheets IETF%s: %s : %s' % (session.meeting.number,
1753+
session.group.acronym,
1754+
sess_time.strftime("%a %H:%M"))
1755+
else:
1756+
name = 'bluesheets-%s-%s' % (session.meeting.number, sess_time.strftime("%Y%m%d%H%M"))
1757+
title = 'Bluesheets %s: %s' % (session.meeting.number, sess_time.strftime("%a %H:%M"))
1758+
doc = Document.objects.create(
1759+
name = name,
1760+
type_id = 'bluesheets',
1761+
title = title,
1762+
group = session.group,
1763+
rev = '00',
1764+
)
1765+
doc.states.add(State.objects.get(type_id='bluesheets',slug='active'))
1766+
DocAlias.objects.create(name=doc.name).docs.add(doc)
1767+
session.sessionpresentation_set.create(document=doc,rev='00')
1768+
filename = '%s-%s%s'% ( doc.name, doc.rev, ext)
1769+
doc.uploaded_filename = filename
1770+
e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev)
1771+
save_error = handle_upload_file(file, filename, session.meeting, 'bluesheets', request=request, encoding=encoding)
1772+
if not save_error:
1773+
doc.save_with_history([e])
1774+
return save_error
1775+
17621776
class UploadMinutesForm(FileUploadForm):
17631777
apply_to_all = forms.BooleanField(label='Apply to all group sessions at this meeting',initial=True,required=False)
17641778

@@ -3024,6 +3038,64 @@ def err(code, text):
30243038
return HttpResponse("Done", status=200, content_type='text/plain')
30253039

30263040

3041+
@require_api_key
3042+
@role_required('Recording Manager', 'Secretariat')
3043+
@csrf_exempt
3044+
def api_upload_bluesheet(request):
3045+
def err(code, text):
3046+
return HttpResponse(text, status=code, content_type='text/plain')
3047+
if request.method == 'POST':
3048+
# parameters:
3049+
# apikey: the poster's personal API key
3050+
# meeting: number as string, i.e., '101', or 'interim-2018-quic-02'
3051+
# group: acronym or special, i.e., 'quic' or 'plenary'
3052+
# item: '1', '2', '3' (the group's first, second, third etc.
3053+
# session during the week)
3054+
# bluesheet: json blob with [{'name': 'Name', 'affiliation': 'Organization', }, ...]
3055+
for item in ['meeting', 'group', 'item', 'bluesheet',]:
3056+
value = request.POST.get(item)
3057+
if not value:
3058+
return err(400, "Missing %s parameter" % item)
3059+
number = request.POST.get('meeting')
3060+
sessions = Session.objects.filter(meeting__number=number)
3061+
if not sessions.exists():
3062+
return err(400, "No sessions found for meeting '%s'" % (number, ))
3063+
acronym = request.POST.get('group')
3064+
sessions = sessions.filter(group__acronym=acronym)
3065+
if not sessions.exists():
3066+
return err(400, "No sessions found in meeting '%s' for group '%s'" % (number, acronym))
3067+
session_times = [ (s.official_timeslotassignment().timeslot.time, s) for s in sessions if s.official_timeslotassignment() ]
3068+
session_times.sort()
3069+
item = request.POST.get('item')
3070+
if not item.isdigit():
3071+
return err(400, "Expected a numeric value for 'item', found '%s'" % (item, ))
3072+
n = int(item)-1 # change 1-based to 0-based
3073+
try:
3074+
time, session = session_times[n]
3075+
except IndexError:
3076+
return err(400, "No item '%s' found in list of sessions for group" % (item, ))
3077+
bjson = request.POST.get('bluesheet')
3078+
try:
3079+
data = json.loads(bjson)
3080+
except json.decoder.JSONDecodeError:
3081+
return err(400, "Invalid json value: '%s'" % (bjson, ))
3082+
3083+
fd, name = tempfile.mkstemp(suffix=".txt", text=True)
3084+
os.close(fd)
3085+
with open(name, "w") as file:
3086+
file.write("Bluesheets for %s\n\n" % session)
3087+
for item in data:
3088+
file.write("{name}\t{affiliation}\n".format(**item))
3089+
with open(name, "br") as file:
3090+
save_err = save_bluesheet(request, session, file)
3091+
if save_err:
3092+
return err(400, save_err)
3093+
else:
3094+
return err(405, "Method not allowed")
3095+
3096+
return HttpResponse("Done", status=200, content_type='text/plain')
3097+
3098+
30273099
def important_dates(request, num=None):
30283100
assert num is None or num.isdigit()
30293101
preview_roles = ['Area Director', 'Secretariat', 'IETF Chair', 'IAD', ]

ietf/secr/proceedings/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ def handle_upload_file(file,filename,meeting,subdir, request=None, encoding=None
5959
"Your upload %s was changed by the sanitization; please check the "
6060
"resulting content. " % (filename, ))
6161
else:
62-
for chunk in file.chunks():
63-
destination.write(chunk)
62+
if hasattr(file, 'chunks'):
63+
for chunk in file.chunks():
64+
destination.write(chunk)
65+
else:
66+
destination.write(file.read())
6467
destination.close()
6568

6669
# unzip zipfile

0 commit comments

Comments
 (0)