Skip to content

Commit fd0df6f

Browse files
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. Commit ready for merge.
- Legacy-Id: 19763
1 parent b04254a commit fd0df6f

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
@@ -436,6 +436,10 @@ def clean_title(self):
436436
return title
437437

438438

439+
class ImportMinutesForm(forms.Form):
440+
markdown_text = forms.CharField(strip=False, widget=forms.HiddenInput)
441+
442+
439443
class RequestMinutesForm(forms.Form):
440444
to = MultiEmailField()
441445
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
@@ -5405,12 +5406,16 @@ def test_iphone_app_json(self):
54055406
self.assertTrue(msessions.filter(group__acronym=s['group']['acronym']).exists())
54065407

54075408
class FinalizeProceedingsTests(TestCase):
5408-
@patch('urllib.request.urlopen')
5409-
def test_finalize_proceedings(self, mock_urlopen):
5410-
mock_urlopen.return_value = BytesIO(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]')
5409+
@override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
5410+
@requests_mock.Mocker()
5411+
def test_finalize_proceedings(self, mock):
54115412
make_meeting_test_data()
54125413
meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last()
54135414
meeting.session_set.filter(group__acronym='mars').first().sessionpresentation_set.create(document=Document.objects.filter(type='draft').first(),rev=None)
5415+
mock.get(
5416+
settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
5417+
text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
5418+
)
54145419

54155420
url = urlreverse('ietf.meeting.views.finalize_proceedings',kwargs={'num':meeting.number})
54165421
login_testing_unauthorized(self,"secretary",url)
@@ -5605,8 +5610,10 @@ def test_upload_minutes_agenda(self):
56055610
self.assertEqual(doc.rev,'02')
56065611

56075612
# Verify that we don't have dead links
5608-
url = url=urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
5613+
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
56095614
top = '/meeting/%s/' % session.meeting.number
5615+
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
5616+
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
56105617
self.crawl_materials(url=url, top=top)
56115618

56125619
def test_upload_minutes_agenda_unscheduled(self):
@@ -5653,8 +5660,10 @@ def test_upload_minutes_agenda_interim(self):
56535660
self.assertEqual(doc.rev,'00')
56545661

56555662
# Verify that we don't have dead links
5656-
url = url=urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
5663+
url = urlreverse('ietf.meeting.views.session_details', kwargs={'num':session.meeting.number, 'acronym': session.group.acronym})
56575664
top = '/meeting/%s/' % session.meeting.number
5665+
self.requests_mock.get(f'{session.notes_url()}/download', text='markdown notes')
5666+
self.requests_mock.get(f'{session.notes_url()}/info', text=json.dumps({'title': 'title', 'updatetime': '2021-12-01T17:11:00z'}))
56585667
self.crawl_materials(url=url, top=top)
56595668

56605669
def test_upload_slides(self):
@@ -5928,6 +5937,151 @@ def test_submit_and_approve_multiple_versions(self):
59285937
self.assertIn('third version', contents)
59295938

59305939

5940+
@override_settings(IETF_NOTES_URL='https://notes.ietf.org/')
5941+
class ImportNotesTests(TestCase):
5942+
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['AGENDA_PATH']
5943+
5944+
def setUp(self):
5945+
super().setUp()
5946+
self.session = SessionFactory(meeting__type_id='ietf')
5947+
self.meeting = self.session.meeting
5948+
5949+
def test_retrieves_note(self):
5950+
"""Can import and preview a note from notes.ietf.org"""
5951+
url = urlreverse('ietf.meeting.views.import_session_minutes',
5952+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
5953+
5954+
self.client.login(username='secretary', password='secretary+password')
5955+
with requests_mock.Mocker() as mock:
5956+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text')
5957+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
5958+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
5959+
r = self.client.get(url)
5960+
self.assertEqual(r.status_code, 200)
5961+
q = PyQuery(r.content)
5962+
iframe = q('iframe#preview')
5963+
self.assertEqual('<p>markdown text</p>', iframe.attr('srcdoc'))
5964+
markdown_text_input = q('form #id_markdown_text')
5965+
self.assertEqual(markdown_text_input.val(), 'markdown text')
5966+
5967+
def test_retrieves_with_broken_metadata(self):
5968+
"""Can import and preview a note even if it has a metadata problem"""
5969+
url = urlreverse('ietf.meeting.views.import_session_minutes',
5970+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
5971+
5972+
self.client.login(username='secretary', password='secretary+password')
5973+
with requests_mock.Mocker() as mock:
5974+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='markdown text')
5975+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info', text='this is not valid json {]')
5976+
r = self.client.get(url)
5977+
self.assertEqual(r.status_code, 200)
5978+
q = PyQuery(r.content)
5979+
iframe = q('iframe#preview')
5980+
self.assertEqual('<p>markdown text</p>', iframe.attr('srcdoc'))
5981+
markdown_text_input = q('form #id_markdown_text')
5982+
self.assertEqual(markdown_text_input.val(), 'markdown text')
5983+
5984+
def test_redirects_on_success(self):
5985+
"""Redirects to session details page after import"""
5986+
url = urlreverse('ietf.meeting.views.import_session_minutes',
5987+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
5988+
5989+
self.client.login(username='secretary', password='secretary+password')
5990+
r = self.client.post(url, {'markdown_text': 'markdown text'})
5991+
self.assertRedirects(
5992+
r,
5993+
urlreverse(
5994+
'ietf.meeting.views.session_details',
5995+
kwargs={
5996+
'num': self.meeting.number,
5997+
'acronym': self.session.group.acronym,
5998+
},
5999+
),
6000+
)
6001+
6002+
def test_imports_previewed_text(self):
6003+
"""Import text that was shown as preview even if notes site is updated"""
6004+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6005+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6006+
6007+
self.client.login(username='secretary', password='secretary+password')
6008+
with requests_mock.Mocker() as mock:
6009+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='updated markdown text')
6010+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
6011+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
6012+
r = self.client.post(url, {'markdown_text': 'original markdown text'})
6013+
self.assertEqual(r.status_code, 302)
6014+
minutes_path = Path(self.meeting.get_materials_path()) / 'minutes'
6015+
with (minutes_path / self.session.minutes().uploaded_filename).open() as f:
6016+
self.assertEqual(f.read(), 'original markdown text')
6017+
6018+
def test_refuses_identical_import(self):
6019+
"""Should not be able to import text identical to the current revision"""
6020+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6021+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6022+
6023+
self.client.login(username='secretary', password='secretary+password')
6024+
r = self.client.post(url, {'markdown_text': 'original markdown text'}) # create a rev
6025+
self.assertEqual(r.status_code, 302)
6026+
with requests_mock.Mocker() as mock:
6027+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text')
6028+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
6029+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
6030+
r = self.client.get(url) # try to import the same text
6031+
self.assertContains(r, "This document is identical", status_code=200)
6032+
q = PyQuery(r.content)
6033+
self.assertEqual(len(q('button:disabled[type="submit"]')), 1)
6034+
self.assertEqual(len(q('button:not(:disabled)[type="submit"]')), 0)
6035+
6036+
def test_handles_missing_previous_revision_file(self):
6037+
"""Should still allow import if the file for the previous revision is missing"""
6038+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6039+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6040+
6041+
self.client.login(username='secretary', password='secretary+password')
6042+
r = self.client.post(url, {'markdown_text': 'original markdown text'}) # create a rev
6043+
# remove the file uploaded for the first rev
6044+
minutes_docs = self.session.sessionpresentation_set.filter(document__type='minutes')
6045+
self.assertEqual(minutes_docs.count(), 1)
6046+
Path(minutes_docs.first().document.get_file_name()).unlink()
6047+
6048+
self.assertEqual(r.status_code, 302)
6049+
with requests_mock.Mocker() as mock:
6050+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/download', text='original markdown text')
6051+
mock.get(f'https://notes.ietf.org/{self.session.notes_id()}/info',
6052+
text=json.dumps({"title": "title", "updatetime": "2021-12-02T11:22:33z"}))
6053+
r = self.client.get(url)
6054+
self.assertEqual(r.status_code, 200)
6055+
q = PyQuery(r.content)
6056+
iframe = q('iframe#preview')
6057+
self.assertEqual('<p>original markdown text</p>', iframe.attr('srcdoc'))
6058+
markdown_text_input = q('form #id_markdown_text')
6059+
self.assertEqual(markdown_text_input.val(), 'original markdown text')
6060+
6061+
def test_handles_note_does_not_exist(self):
6062+
"""Should not try to import a note that does not exist"""
6063+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6064+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6065+
6066+
self.client.login(username='secretary', password='secretary+password')
6067+
with requests_mock.Mocker() as mock:
6068+
mock.get(requests_mock.ANY, status_code=404)
6069+
r = self.client.get(url, follow=True)
6070+
self.assertContains(r, 'Could not import', status_code=200)
6071+
6072+
def test_handles_notes_server_failure(self):
6073+
"""Problems communicating with the notes server should be handled gracefully"""
6074+
url = urlreverse('ietf.meeting.views.import_session_minutes',
6075+
kwargs={'num': self.meeting.number, 'session_id': self.session.pk})
6076+
self.client.login(username='secretary', password='secretary+password')
6077+
6078+
with requests_mock.Mocker() as mock:
6079+
mock.get(re.compile(r'.+/download'), exc=requests.exceptions.ConnectTimeout)
6080+
mock.get(re.compile(r'.+//info'), text='{}')
6081+
r = self.client.get(url, follow=True)
6082+
self.assertContains(r, 'Could not reach the notes server', status_code=200)
6083+
6084+
59316085
class SessionTests(TestCase):
59326086

59336087
def test_meeting_requests(self):
@@ -6911,27 +7065,34 @@ def test_proceedings_acknowledgements_link(self):
69117065
0,
69127066
)
69137067

6914-
@patch('ietf.meeting.utils.requests.get')
6915-
def test_proceedings_attendees(self, mockobj):
6916-
mockobj.return_value.text = b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]'
6917-
mockobj.return_value.json = lambda: json.loads(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]')
7068+
@override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
7069+
@requests_mock.Mocker()
7070+
def test_proceedings_attendees(self, mock):
69187071
make_meeting_test_data()
69197072
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
7073+
mock.get(
7074+
settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
7075+
text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
7076+
)
69207077
finalize(meeting)
69217078
url = urlreverse('ietf.meeting.views.proceedings_attendees',kwargs={'num':97})
69227079
response = self.client.get(url)
69237080
self.assertContains(response, 'Attendee List')
69247081
q = PyQuery(response.content)
69257082
self.assertEqual(1,len(q("#id_attendees tbody tr")))
69267083

6927-
@patch('urllib.request.urlopen')
6928-
def test_proceedings_overview(self, mock_urlopen):
7084+
@override_settings(STATS_REGISTRATION_ATTENDEES_JSON_URL='https://ietf.example.com/{number}')
7085+
@requests_mock.Mocker()
7086+
def test_proceedings_overview(self, mock):
69297087
'''Test proceedings IETF Overview page.
69307088
Note: old meetings aren't supported so need to add a new meeting then test.
69317089
'''
6932-
mock_urlopen.return_value = BytesIO(b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US"}]')
69337090
make_meeting_test_data()
69347091
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="97")
7092+
mock.get(
7093+
settings.STATS_REGISTRATION_ATTENDEES_JSON_URL.format(number=meeting.number),
7094+
text=json.dumps([{"LastName": "Smith", "FirstName": "John", "Company": "ABC", "Country": "US"}]),
7095+
)
69357096
finalize(meeting)
69367097
url = urlreverse('ietf.meeting.views.proceedings_overview',kwargs={'num':97})
69377098
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)