diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 211e35e9a35f6e4a6e4f9b2972da6cdabab1e5c6..462ff5a47e42248ed0102f29843e2b16c3bcb33e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,8 +7,6 @@ stages: execute_ipynbs: stage: execute - before_script: - - pip install notedown script: - export PYTHONPATH=$PYTHONPATH:${PWD}/code - export OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 MKL_DYNAMIC=FALSE @@ -47,7 +45,7 @@ mirror to github: edx archive: stage: build - script: ./scripts/converter.py --silent ./generated/with_output + script: ./scripts/converter.py ./generated/with_output artifacts: paths: - generated/import_to_edx.tar.gz diff --git a/code/edx_components.py b/code/edx_components.py index c4352a14adc84955a19ff434b075b7314d6b17e9..d01b67e93a624db113a4a2017dcbd821cfdc8641 100644 --- a/code/edx_components.py +++ b/code/edx_components.py @@ -1,20 +1,43 @@ import sys import os +import secrets + +from xml.etree.ElementTree import Element, SubElement +from xml.etree import ElementTree + from hashlib import md5 +from IPython import display +import feedparser + module_dir = os.path.dirname(__file__) sys.path.extend(module_dir) -from IPython import display -import feedparser -__all__ = ['MoocVideo', 'PreprintReference', 'MoocDiscussion', - 'MoocCheckboxesAssessment', 'MoocMultipleChoiceAssessment', - 'MoocPeerAssessment', 'MoocSelfAssessment'] +__all__ = [ + 'MoocVideo', 'PreprintReference', 'MoocDiscussion', + 'MoocCheckboxesAssessment', 'MoocMultipleChoiceAssessment', + 'MoocSelfAssessment' +] + + +def _add_solution(el, text): + """Add a solution xml to a problem.""" + sol = SubElement(el, 'solution') + div = SubElement(sol, 'div') + div.attrib['class'] = 'detailed-solution' + title = SubElement(div, 'p') + title.text = 'Explanation' + content = SubElement(div, 'p') + content.text = text + class MoocComponent: - def __repr__(self): - return '{0}(**{1})'.format(self.__class__.__name__, repr(self.param)) + def _repr_mimebundle_(self, include, exclude): + return { + 'application/vnd.edx.olxml+xml': + ElementTree.tostring(self.xml, encoding='unicode'), + } class MoocVideo(MoocComponent, display.YouTubeVideo): @@ -22,27 +45,34 @@ class MoocVideo(MoocComponent, display.YouTubeVideo): download_track='true', download_video='true', show_captions='true', **kwargs): """A video component of an EdX mooc embeddable in IPython notebook.""" - tmp = locals() - del tmp['kwargs'], tmp['self'], tmp['__class__'] - del tmp['id'], tmp['src_location'], tmp['res'] - kwargs.update(tmp) - kwargs['youtube_id_1_0'] = id - kwargs['youtube'] = "1.00:" + id - - # Add source if provided - loc = ("http://delftxdownloads.tudelft.nl/" - "TOPOCMx-QuantumKnots/TOPOCMx-{0}-video.{1}.mp4") + + self.xml = Element('video', attrib=dict( + youtube=f'1.00:{id}', + youtube_id_1_0=id, + display_name=display_name, + download_track=download_track, + download_video=download_video, + show_captions=show_captions, + **kwargs, + )) + if src_location is not None: - kwargs['source'] = loc.format(src_location, res) + self.xml.attrib['source'] = ( + "http://delftxdownloads.tudelft.nl/TOPOCMx-QuantumKnots/" + f"TOPOCMx-{src_location}-video.{res}.mp4" + ) - self.param = kwargs super().__init__(id, rel=0, cc_load_policy=1) def _repr_html_(self): orig = super()._repr_html_() - return ('
{}
' - .format(orig.replace('{}' + .format(orig.replace( + '' - s += '

http://arxiv.org/abs/%s
' % (ind, ind) + s += f'

http://arxiv.org/abs/%s
' % (ind, ind) s += '

' s += ", ".join(author.name for author in data['entries'][0]['authors']) @@ -78,49 +108,26 @@ class PreprintReference: return s -class MoocPeerAssessment(MoocComponent): - def __init__(self, must_grade=5, must_be_graded_by=3, due=9, review_due=16, - url_name=None, **kwargs): - - self.placeholder = ('

Read one of the above papers and see how it is\n' - 'related to the current topic.

\n' - '

In the live version of the course, you would ' - 'need to write a summary which is then assessed by ' - 'your peers.

') - - tmp = locals() - del tmp['kwargs'], tmp['self'] - kwargs.update(tmp) - - with open(module_dir + '/xmls/openassessment_peer.xml', 'r') as content_file: - openassessment_peer = content_file.read() - - kwargs['openassessment_peer'] = openassessment_peer - - self.param = kwargs - - def _repr_html_(self): - return self.placeholder - - class MoocSelfAssessment(MoocComponent): - def __init__(self, due=9, review_due=16, url_name=None, **kwargs): - - tmp = locals() - del tmp['kwargs'], tmp['self'] - kwargs.update(tmp) + def __init__(self): + self.placeholder = ( + '

MoocSelfAssessment description

\n' + '

In the live version of the course, you would ' + 'need to share your solution and grade yourself.' + '

' + ) - self.placeholder = ('

MoocSelfAssessment description

\n' - '

In the live version of the course, you would ' - 'need to share your solution and grade yourself.' - '

') + content_filename = module_dir + '/xmls/openassessment_self.xml' + with open(content_filename, 'r') as content_file: + self.xml = xml = ElementTree.fromstring(content_file.read()) - with open(module_dir + '/xmls/openassessment_self.xml', 'r') as content_file: - openassessment_self = content_file.read() + assessment = xml.find('assessments').find('assessment') - kwargs['openassessment_self'] = openassessment_self + xml.attrib['submission_start'] = '2001-12-31T10:00:00Z' + xml.attrib['submission_due'] = '2100-12-31T10:00:00Z' - self.param = kwargs + assessment.attrib['start'] = '2001-12-31T10:00:00Z' + assessment.attrib['due'] = '2100-12-31T10:00:00Z' def _repr_html_(self): return self.placeholder @@ -128,7 +135,7 @@ class MoocSelfAssessment(MoocComponent): class MoocCheckboxesAssessment(MoocComponent): def __init__(self, question, answers, correct_answers, max_attempts=2, - display_name="Question", **kwargs): + display_name="Question", explanation=None): """ MoocCheckboxesAssessment component @@ -140,31 +147,51 @@ class MoocCheckboxesAssessment(MoocComponent): correct_answers : list of int """ - tmp = locals() - del tmp['kwargs'], tmp['self'] - kwargs.update(tmp) + self.xml = xml = Element('problem', attrib=dict( + display_name=display_name, + max_attempts=str(max_attempts), + )) + + SubElement(xml, 'p', text='question') + SubElement(xml, 'p', text='Select the answers that match') + + sub = SubElement(xml, 'choiceresponse') + sub = SubElement(sub, 'checkboxgroup') + sub.attrib['label'] = "Select the answers that match" + sub.attrib['direction'] = "vertical" + + for i, ans in enumerate(answers): + choice = SubElement(sub, 'choice') + if i in correct_answers: + choice.attrib['correct'] = 'true' + else: + choice.attrib['correct'] = 'false' + choice.text = ans + + if explanation is not None: + _add_solution(xml, explanation) + + self.question = question + self.answers = answers + self.correct_answers = correct_answers + self.explanation = explanation - self.param = kwargs - - def _get_html_repr(self): - param = self.param - s = '

%s

' % param['question'] + def _repr_html_(self): + s = '

%s

' % self.question s += '
' - s += '
'.join(param['answers']) + s += '
'.join(self.answers) s += '
' - answer = 'The correct answer{0}:
'.format('s are' if - len(param['correct_answers']) > 1 - else ' is') + answer = 'The correct answer{0}:
'.format( + 's are' if len(self.correct_answers) > 1 else ' is' + ) tmp = [] - for i in param['correct_answers']: - tmp.append(param['answers'][i]) + for i in self.correct_answers: + tmp.append(self.answers[i]) answer += ', '.join(t for t in tmp) answer += '.' - try: - answer += '
' + param['explanation'] + '' - except KeyError: - pass + if self.explanation is not None: + answer += '
' + self.explanation + '' s += """
""" - return s.format(md5(repr(self).encode('utf-8')).hexdigest(), answer) - - def _repr_html_(self): - return self._get_html_repr() + return s.format(secrets.token_urlsafe(20), answer) class MoocMultipleChoiceAssessment(MoocComponent): def __init__(self, question, answers, correct_answer, max_attempts=2, - display_name="Question", **kwargs): + display_name="Question", explanation=None): """ MoocMultipleChoiceAssessment component @@ -196,26 +220,46 @@ class MoocMultipleChoiceAssessment(MoocComponent): correct_answers : int """ - tmp = locals() - del tmp['kwargs'], tmp['self'] - kwargs.update(tmp) + self.xml = xml = Element('problem', attrib=dict( + display_name=display_name, + max_attempts=str(max_attempts), + )) + + SubElement(xml, 'p', text=question) + SubElement(xml, 'p', text='Please select correct answer') + + sub = SubElement(xml, 'multiplechoiceresponse') + sub = SubElement(sub, 'choicegroup') + sub.attrib['label'] = "Please select correct answer" + sub.attrib['type'] = "MultipleChoice" + + for i, ans in enumerate(answers): + choice = SubElement(sub, 'choice') + if i == correct_answer: + choice.attrib['correct'] = 'true' + else: + choice.attrib['correct'] = 'false' + choice.text = ans + + if explanation is not None: + _add_solution(xml, explanation) + + self.question = question + self.answers = answers + self.correct_answer = correct_answer + self.explanation = explanation - self.param = kwargs - - def _get_html_repr(self): - param = self.param - s = '

%s

' % param['question'] - s+= '
' - for ans in param['answers']: - s += '' % ans + ans + '
' - s+= '
' + def _repr_html_(self): + s = '

%s

' % self.question + s += '
' + for ans in self.answers: + s += f'{ans}
' + s += '
' answer = 'The correct answer is:
' - answer += param['answers'][param['correct_answer']] - try: - answer += '
' + param['explanation'] + '' - except KeyError: - pass + answer += self.answers[self.correct_answer] + if self.explanation is not None: + answer += f'
' + self.explanation + '' s += """ """ - return s.format(md5(repr(self).encode('utf-8')).hexdigest(), answer) - - def _repr_html_(self): - return self._get_html_repr() + return s.format(secrets.token_urlsafe(20), answer) class MoocDiscussion(MoocComponent): - def __init__(self, discussion_category, discussion_target, display_name=None, - discussion_id=None, **kwargs): - - tmp = locals() - del tmp['kwargs'], tmp['self'] - kwargs.update(tmp) - - if display_name is None: - kwargs['display_name'] = discussion_target - - if discussion_id is None: - kwargs['discussion_id'] = md5(discussion_category.encode('utf-8') + discussion_target.encode('utf-8')).hexdigest() - - self.param = kwargs + def __init__( + self, discussion_category, discussion_target, display_name=None, + discussion_id=None, **kwargs, + ): + discussion_id = md5( + discussion_category.encode('utf-8') + + discussion_target.encode('utf-8') + ).hexdigest() + self.xml = Element('discussion', attrib=dict( + discussion_category=discussion_category, + discussion_target=discussion_target, + display_name=(display_name or discussion_target), + discussion_id=discussion_id, + url_name=discussion_id, + **kwargs + )) + self.display_name = (display_name or discussion_target) def _repr_html_(self): - return "

Discussion entitled '{0}' is available in the online version of the course.

".format(self.param['display_name']) + return ( + f"

Discussion {self.display_name}" + " is available in the EdX version of the course.

" + ) diff --git a/docker/python3.yaml b/docker/python3.yaml index 558c561962f7ce7589c2ae68b30c9874ee3767ee..290744a1a73b68e46b202c1a8f68b233ec172a9e 100644 --- a/docker/python3.yaml +++ b/docker/python3.yaml @@ -5,11 +5,12 @@ dependencies: - python=3.6 - matplotlib=2.0.0 - kwant=1.3* # Untill v1.3 is out - - scipy=0.18* + - scipy=1.1* - holoviews=1.7* - feedparser=5.2* - sympy=1.0* - pelican=3.7* + - ipython>=6.1.0 - notebook - markdown - notedown diff --git a/edx_skeleton/chapter/.keep b/edx_skeleton/chapter/.keep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/edx_skeleton/chapter/sec_00.xml b/edx_skeleton/chapter/sec_00.xml deleted file mode 100644 index 669c0a93c55ae7e4b73a4808e4d361cceb77aef2..0000000000000000000000000000000000000000 --- a/edx_skeleton/chapter/sec_00.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/edx_skeleton/chapter/sec_05.xml b/edx_skeleton/chapter/sec_05.xml deleted file mode 100644 index 3a5501f556d543807925dd116e48504f10cb7899..0000000000000000000000000000000000000000 --- a/edx_skeleton/chapter/sec_05.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/edx_skeleton/course.xml b/edx_skeleton/course.xml index e9658439153d01198e18b1b5ae3a92bbe1f4f7b1..d692e06e4b10b687459e3eea569987b97e4ac852 100644 --- a/edx_skeleton/course.xml +++ b/edx_skeleton/course.xml @@ -1 +1,7 @@ - \ No newline at end of file + diff --git a/edx_skeleton/course/.keep b/edx_skeleton/course/.keep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/edx_skeleton/discussion/.keep b/edx_skeleton/discussion/.keep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/edx_skeleton/html/subsec_00_01_00_out_00.html b/edx_skeleton/html/subsec_00_01_00_out_00.html deleted file mode 100644 index b940105d80ed76023162c604a2759f732a600dde..0000000000000000000000000000000000000000 --- a/edx_skeleton/html/subsec_00_01_00_out_00.html +++ /dev/null @@ -1 +0,0 @@ -

diff --git a/edx_skeleton/html/subsec_00_01_00_out_00.xml b/edx_skeleton/html/subsec_00_01_00_out_00.xml deleted file mode 100644 index 74d63f6f23c5a087843e47fb316ebe0e035e6c8a..0000000000000000000000000000000000000000 --- a/edx_skeleton/html/subsec_00_01_00_out_00.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/edx_skeleton/html/subsec_05_03_00_out_00.html b/edx_skeleton/html/subsec_05_03_00_out_00.html deleted file mode 100644 index b940105d80ed76023162c604a2759f732a600dde..0000000000000000000000000000000000000000 --- a/edx_skeleton/html/subsec_05_03_00_out_00.html +++ /dev/null @@ -1 +0,0 @@ -

diff --git a/edx_skeleton/html/subsec_05_03_00_out_00.xml b/edx_skeleton/html/subsec_05_03_00_out_00.xml deleted file mode 100644 index 76c0feaf30442d54bad2f3de8479c2c018fd8a07..0000000000000000000000000000000000000000 --- a/edx_skeleton/html/subsec_05_03_00_out_00.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/edx_skeleton/problem/.keep b/edx_skeleton/problem/.keep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/edx_skeleton/sequential/.keep b/edx_skeleton/sequential/.keep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/edx_skeleton/sequential/subsec_00_01.xml b/edx_skeleton/sequential/subsec_00_01.xml deleted file mode 100644 index 76754da1544b7b39ed5f2354de5a67f4787333ef..0000000000000000000000000000000000000000 --- a/edx_skeleton/sequential/subsec_00_01.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/edx_skeleton/sequential/subsec_05_03.xml b/edx_skeleton/sequential/subsec_05_03.xml deleted file mode 100644 index dea05cb0d96532260b9d6652f3e6b0323cb045a7..0000000000000000000000000000000000000000 --- a/edx_skeleton/sequential/subsec_05_03.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/edx_skeleton/vertical/.keep b/edx_skeleton/vertical/.keep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/edx_skeleton/vertical/subsec_00_01_00.xml b/edx_skeleton/vertical/subsec_00_01_00.xml deleted file mode 100644 index 2ba59c65cda7b2d6f2ec97f14443004b6350aaba..0000000000000000000000000000000000000000 --- a/edx_skeleton/vertical/subsec_00_01_00.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/edx_skeleton/vertical/subsec_05_03_00.xml b/edx_skeleton/vertical/subsec_05_03_00.xml deleted file mode 100644 index dc7bda91ec7a8e60aa98ede3aba2d84218eba913..0000000000000000000000000000000000000000 --- a/edx_skeleton/vertical/subsec_05_03_00.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/edx_skeleton/video/.keep b/edx_skeleton/video/.keep deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/scripts/converter.py b/scripts/converter.py index 11118cba12d954ba47073d9085cfba59d44c24b5..547214b75e15dd62ea87c8d90280009af6528a83 100755 --- a/scripts/converter.py +++ b/scripts/converter.py @@ -11,11 +11,9 @@ import tempfile from time import strptime from types import SimpleNamespace import shutil -import subprocess import urllib.request -from xml.etree.ElementTree import Element, SubElement +from xml.etree.ElementTree import SubElement from xml.etree import ElementTree -from xml.dom import minidom import nbformat from nbformat import v4 as current @@ -43,8 +41,7 @@ url = ( "https://cdnjs.cloudflare.com/ajax/libs" "/iframe-resizer/3.5.14/iframeResizer.min.js" ) -response = urllib.request.urlopen(url) -js = response.read().decode('utf-8') +js = urllib.request.urlopen(url).read().decode('utf-8') IFRAME_TEMPLATE = r"""