Skip to content

Commit 9b54c9d

Browse files
committed
Merged in [19763] from jennifer@painless-security.com:
Add ability to import session minutes from notes.ietf.org. Mock out calls to the requests library in tests. Call markdown library through a util method. Fixes ietf-tools#3489. - Legacy-Id: 19767 Note: SVN reference [19763] has been migrated to Git commit fd0df6f
2 parents 47aadd0 + fd0df6f commit 9b54c9d

21 files changed

Lines changed: 750 additions & 188 deletions

ietf/doc/views_bofreq.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import debug # pyflakes:ignore
44

55
import io
6-
import markdown
76

87
from django import forms
98
from django.contrib.auth.decorators import login_required
@@ -20,6 +19,7 @@
2019
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
2120
from ietf.ietfauth.utils import has_role, role_required
2221
from ietf.person.fields import SearchablePersonsField
22+
from ietf.utils import markdown
2323
from ietf.utils.response import permission_denied
2424
from ietf.utils.text import xslugify
2525
from ietf.utils.textupload import get_cleaned_text_file_content
@@ -64,7 +64,7 @@ def require_field(f):
6464
if require_field("bofreq_file"):
6565
content = get_cleaned_text_file_content(self.cleaned_data["bofreq_file"])
6666
try:
67-
_ = markdown.markdown(content, extensions=['extra'])
67+
_ = markdown.markdown(content)
6868
except Exception as e:
6969
raise forms.ValidationError(f'Markdown processing failed: {e}')
7070

ietf/doc/views_doc.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
import json
4141
import os
4242
import re
43-
import markdown
4443

4544
from urllib.parse import quote
4645

@@ -80,7 +79,7 @@
8079
from ietf.review.models import ReviewAssignment
8180
from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs
8281
from ietf.review.utils import no_review_from_teams_on_doc
83-
from ietf.utils import markup_txt, log
82+
from ietf.utils import markup_txt, log, markdown
8483
from ietf.utils.draft import Draft
8584
from ietf.utils.response import permission_denied
8685
from ietf.utils.text import maybe_split
@@ -550,7 +549,7 @@ def document_main(request, name, rev=None):
550549
))
551550

552551
if doc.type_id == "bofreq":
553-
content = markdown.markdown(doc.text_or_error(),extensions=['extra'])
552+
content = markdown.markdown(doc.text_or_error())
554553
editors = bofreq_editors(doc)
555554
responsible = bofreq_responsible(doc)
556555
can_manage = has_role(request.user,['Secretariat', 'Area Director', 'IAB'])
@@ -661,7 +660,7 @@ def document_main(request, name, rev=None):
661660
content = doc.text_or_error()
662661
t = "plain text"
663662
elif extension == ".md":
664-
content = markdown.markdown(doc.text_or_error(), extensions=['extra'])
663+
content = markdown.markdown(doc.text_or_error())
665664
content_is_html = True
666665
t = "markdown"
667666
other_types.append((t, url))

ietf/group/views.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import datetime
3939
import itertools
4040
import io
41-
import markdown
4241
import math
4342
import os
4443
import re
@@ -121,7 +120,7 @@
121120
from ietf.utils.pipe import pipe
122121
from ietf.utils.response import permission_denied
123122
from ietf.utils.text import strip_suffix
124-
123+
from ietf.utils import markdown
125124

126125

127126
# --- Helpers ----------------------------------------------------------
@@ -581,7 +580,7 @@ def group_about_rendertest(request, acronym, group_type=None):
581580
if group.charter:
582581
charter = get_charter_text(group)
583582
try:
584-
rendered = markdown.markdown(charter, extensions=['extra'])
583+
rendered = markdown.markdown(charter)
585584
except Exception as e:
586585
rendered = f'Markdown rendering failed: {e}'
587586
return render(request, 'group/group_about_rendertest.html', {'group':group, 'charter':charter, 'rendered':rendered})

ietf/meeting/forms.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ def clean_title(self):
434434
return title
435435

436436

437+
class ImportMinutesForm(forms.Form):
438+
markdown_text = forms.CharField(strip=False, widget=forms.HiddenInput)
439+
440+
437441
class RequestMinutesForm(forms.Form):
438442
to = MultiEmailField()
439443
cc = MultiEmailField(required=False)

ietf/meeting/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from collections import namedtuple
1616
from pathlib import Path
17+
from urllib.parse import urljoin
1718

1819
import debug # pyflakes:ignore
1920

@@ -1260,6 +1261,13 @@ def jabber_room_name(self):
12601261
else:
12611262
return self.group.acronym
12621263

1264+
def notes_id(self):
1265+
note_id_fragment = 'plenary' if self.type.slug == 'plenary' else self.group.acronym
1266+
return f'notes-ietf-{self.meeting.number}-{note_id_fragment}'
1267+
1268+
def notes_url(self):
1269+
return urljoin(settings.IETF_NOTES_URL, self.notes_id())
1270+
12631271
class SchedulingEvent(models.Model):
12641272
session = ForeignKey(Session)
12651273
time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened")

ietf/meeting/tests_views.py

Lines changed: 174 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import re
99
import shutil
1010
import pytz
11+
import requests.exceptions
12+
import requests_mock
1113

1214
from unittest import skipIf
1315
from mock import patch, PropertyMock
@@ -19,7 +21,6 @@
1921
from PIL import Image
2022
from pathlib import Path
2123

22-
2324
from django.urls import reverse as urlreverse
2425
from django.conf import settings
2526
from django.contrib.auth.models import User
@@ -5444,12 +5445,16 @@ def test_iphone_app_json(self):
54445445
self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists())
54455446

54465447
class FinalizeProceedingsTests(TestCase):
5447-
@patch('urllib.request.urlopen')
5448-
def test_finalize_proceedings(self, mock_urlopen):
5449-
mock_urlopen.return_value = BytesIO(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]')
5448+
@override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
5449+
@requests_mock.Mocker()
5450+
def test_finalize_proceedings(self, mock):
54505451
make_meeting_test_data()
54515452
meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last()
54525453
meeting.session_set.filter(group__acronym='mars').first().sessionpresentation_set.create(document=Document.objects.filter(type='draft').first(),rev=None)
5454+
mock.get(
5455+
settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
5456+
text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
5457+
)
54535458

54545459
url = urlreverse('ietf.meeting.views.finalize_proceedings',kwargs={'num':meeting.number})
54555460
login_testing_unauthorized(self,"secretary",url)
@@ -5644,8 +5649,10 @@ def test_upload_minutes_agenda(self):
56445649
self.assertEqual(doc.rev,'02')
56455650

56465651
# Verify that we don't have dead links
5647-
url = url=urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
5652+
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
56485653
top = '/meeting/%s/' % session.meeting.number
5654+
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
5655+
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
56495656
self.crawl_materials(url=url, top=top)
56505657

56515658
def test_upload_minutes_agenda_unscheduled(self):
@@ -5692,8 +5699,10 @@ def test_upload_minutes_agenda_interim(self):
56925699
self.assertEqual(doc.rev,'00')
56935700

56945701
# Verify that we don't have dead links
5695-
url = url=urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
5702+
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
56965703
top = '/meeting/%s/' % session.meeting.number
5704+
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
5705+
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
56975706
self.crawl_materials(url=url, top=top)
56985707

56995708
def test_upload_slides(self):
@@ -5967,6 +5976,151 @@ def test_submit_and_approve_multiple_versions(self):
59675976
self.assertIn('third version', contents)
59685977

59695978

5979+
@override_settings(IETF_NOTES_URL='https://notes.ietf.org/')
5980+
class ImportNotesTests(TestCase):
5981+
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
5982+
5983+
def setUp(self):
5984+
super().setUp()
5985+
self.session = SessionFactory(meeting__type_id='ietf')
5986+
self.meeting = self.session.meeting
5987+
5988+
def test_retrieves_note(self):
5989+
"""Can import and preview a note from notes.ietf.org"""
5990+
url = urlreverse('ietf.meeting.views.import_session_minutes',
5991+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
5992+
5993+
self.client.login(username='secretary', password='secretary+password')
5994+
with requests_mock.Mocker() as mock:
5995+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text')
5996+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
5997+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
5998+
r = self.client.get(url)
5999+
self.assertEqual(r.status_code, 200)
6000+
q = PyQuery(r.content)
6001+
iframe = q('iframe#preview')
6002+
self.assertEqual('<p>markdown text</p>', iframe.attr('srcdoc'))
6003+
markdown_text_input = q('form #id_markdown_text')
6004+
self.assertEqual(markdown_text_input.val(), 'markdown text')
6005+
6006+
def test_retrieves_with_broken_metadata(self):
6007+
"""Can import and preview a note even if it has a metadata problem"""
6008+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6009+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6010+
6011+
self.client.login(username='secretary', password='secretary+password')
6012+
with requests_mock.Mocker() as mock:
6013+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text')
6014+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text='this is not valid json {]')
6015+
r = self.client.get(url)
6016+
self.assertEqual(r.status_code, 200)
6017+
q = PyQuery(r.content)
6018+
iframe = q('iframe#preview')
6019+
self.assertEqual('<p>markdown text</p>', iframe.attr('srcdoc'))
6020+
markdown_text_input = q('form #id_markdown_text')
6021+
self.assertEqual(markdown_text_input.val(), 'markdown text')
6022+
6023+
def test_redirects_on_success(self):
6024+
"""Redirects to session details page after import"""
6025+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6026+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6027+
6028+
self.client.login(username='secretary', password='secretary+password')
6029+
r = self.client.post(url, {'markdown_text': 'markdown text'})
6030+
self.assertRedirects(
6031+
r,
6032+
urlreverse(
6033+
'ietf.meeting.views.session_details',
6034+
kwargs={
6035+
'num': self.meeting.number,
6036+
'acronym': self.session.group.acronym,
6037+
},
6038+
),
6039+
)
6040+
6041+
def test_imports_previewed_text(self):
6042+
"""Import text that was shown as preview even if notes site is updated"""
6043+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6044+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6045+
6046+
self.client.login(username='secretary', password='secretary+password')
6047+
with requests_mock.Mocker() as mock:
6048+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='updated markdown text')
6049+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
6050+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
6051+
r = self.client.post(url, {'markdown_text': 'original markdown text'})
6052+
self.assertEqual(r.status_code, 302)
6053+
minutes_path = Path(self.meeting.get_materials_path()) / 'minutes'
6054+
with (minutes_path / self.session.minutes().uploaded_filename).open() as f:
6055+
self.assertEqual(f.read(), 'original markdown text')
6056+
6057+
def test_refuses_identical_import(self):
6058+
"""Should not be able to import text identical to the current revision"""
6059+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6060+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6061+
6062+
self.client.login(username='secretary', password='secretary+password')
6063+
r = self.client.post(url, {'markdown_text': 'original markdown text'}) # create a rev
6064+
self.assertEqual(r.status_code, 302)
6065+
with requests_mock.Mocker() as mock:
6066+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text')
6067+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
6068+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
6069+
r = self.client.get(url) # try to import the same text
6070+
self.assertContains(r, "This document is identical", status_code=200)
6071+
q = PyQuery(r.content)
6072+
self.assertEqual(len(q('button:disabled[type="submit"]')), 1)
6073+
self.assertEqual(len(q('button:not(:disabled)[type="submit"]')), 0)
6074+
6075+
def test_handles_missing_previous_revision_file(self):
6076+
"""Should still allow import if the file for the previous revision is missing"""
6077+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6078+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6079+
6080+
self.client.login(username='secretary', password='secretary+password')
6081+
r = self.client.post(url, {'markdown_text': 'original markdown text'}) # create a rev
6082+
# remove the file uploaded for the first rev
6083+
minutes_docs = self.session.sessionpresentation_set.filter(document__type='minutes')
6084+
self.assertEqual(minutes_docs.count(), 1)
6085+
Path(minutes_docs.first().document.get_file_name()).unlink()
6086+
6087+
self.assertEqual(r.status_code, 302)
6088+
with requests_mock.Mocker() as mock:
6089+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text')
6090+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
6091+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
6092+
r = self.client.get(url)
6093+
self.assertEqual(r.status_code, 200)
6094+
q = PyQuery(r.content)
6095+
iframe = q('iframe#preview')
6096+
self.assertEqual('<p>original markdown text</p>', iframe.attr('srcdoc'))
6097+
markdown_text_input = q('form #id_markdown_text')
6098+
self.assertEqual(markdown_text_input.val(), 'original markdown text')
6099+
6100+
def test_handles_note_does_not_exist(self):
6101+
"""Should not try to import a note that does not exist"""
6102+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6103+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6104+
6105+
self.client.login(username='secretary', password='secretary+password')
6106+
with requests_mock.Mocker() as mock:
6107+
mock.get(requests_mock.ANY, status_code=404)
6108+
r = self.client.get(url, follow=True)
6109+
self.assertContains(r, 'Could not import', status_code=200)
6110+
6111+
def test_handles_notes_server_failure(self):
6112+
"""Problems communicating with the notes server should be handled gracefully"""
6113+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6114+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6115+
self.client.login(username='secretary', password='secretary+password')
6116+
6117+
with requests_mock.Mocker() as mock:
6118+
mock.get(re.compile(r'.+/download'), exc=requests.exceptions.ConnectTimeout)
6119+
mock.get(re.compile(r'.+//info'), text='{}')
6120+
r = self.client.get(url, follow=True)
6121+
self.assertContains(r, 'Could not reach the notes server', status_code=200)
6122+
6123+
59706124
class SessionTests(TestCase):
59716125

59726126
def test_meeting_requests(self):
@@ -6950,27 +7104,34 @@ def test_proceedings_acknowledgements_link(self):
69507104
0,
69517105
)
69527106

6953-
@patch('ietf.meeting.utils.requests.get')
6954-
def test_proceedings_attendees(self, mockobj):
6955-
mockobj.return_value.text = b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]'
6956-
mockobj.return_value.json = lambda: json.loads(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]')
7107+
@override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
7108+
@requests_mock.Mocker()
7109+
def test_proceedings_attendees(self, mock):
69577110
make_meeting_test_data()
69587111
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
7112+
mock.get(
7113+
settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
7114+
text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
7115+
)
69597116
finalize(meeting)
69607117
url = urlreverse('ietf.meeting.views.proceedings_attendees',kwargs={'num':97})
69617118
response = self.client.get(url)
69627119
self.assertContains(response, 'Attendee List')
69637120
q = PyQuery(response.content)
69647121
self.assertEqual(1,len(q("#id_attendees tbody tr")))
69657122

6966-
@patch('urllib.request.urlopen')
6967-
def test_proceedings_overview(self, mock_urlopen):
7123+
@override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
7124+
@requests_mock.Mocker()
7125+
def test_proceedings_overview(self, mock):
69687126
'''Test proceedings IETF Overview page.
69697127
Note: old meetings aren't supported so need to add a new meeting then test.
69707128
'''
6971-
mock_urlopen.return_value = BytesIO(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]')
69727129
make_meeting_test_data()
69737130
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
7131+
mock.get(
7132+
settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
7133+
text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
7134+
)
69747135
finalize(meeting)
69757136
url = urlreverse('ietf.meeting.views.proceedings_overview',kwargs={'num':97})
69767137
response = self.client.get(url)

ietf/meeting/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
url(r'^session/(?P<session_id>\d+)/bluesheets$', views.upload_session_bluesheets),
1414
url(r'^session/(?P<session_id>\d+)/minutes$', views.upload_session_minutes),
1515
url(r'^session/(?P<session_id>\d+)/agenda$', views.upload_session_agenda),
16+
url(r'^session/(?P<session_id>\d+)/import/minutes$', views.import_session_minutes),
1617
url(r'^session/(?P<session_id>\d+)/propose_slides$', views.propose_session_slides),
1718
url(r'^session/(?P<session_id>\d+)/slides(?:/%(name)s)?$' % settings.URL_REGEXPS, views.upload_session_slides),
1819
url(r'^session/(?P<session_id>\d+)/add_to_session$', views.ajax_add_slides_to_session),

0 commit comments

Comments
 (0)