diff --git a/docs/index.rst b/docs/index.rst index a943a2b..81cf5ea 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ Tutorials tutorials/writing_content tutorials/styling_content tutorials/text_replacements + tutorials/uploading_files tutorials/publishing_content diff --git a/docs/tutorials/text_replacements.rst b/docs/tutorials/text_replacements.rst index dab0155..e670aa3 100644 --- a/docs/tutorials/text_replacements.rst +++ b/docs/tutorials/text_replacements.rst @@ -25,6 +25,8 @@ Note that `_course_metadata/replacements.json` is just a regular old JSON file. Usage -------- +Keep in mind that the order of replacement is unspecified. Thus, it is important to choose keys that will not appear within values, and will not appear within source documents where replacement is undesired. + Custom text replacements per-content -------------------------------------- diff --git a/docs/tutorials/uploading_files.rst b/docs/tutorials/uploading_files.rst new file mode 100644 index 0000000..4a97c26 --- /dev/null +++ b/docs/tutorials/uploading_files.rst @@ -0,0 +1,42 @@ +How to upload a file +-------------------------------------------------------------------------- + + + +The `meta.json` file +==================== + +Filenames and titles of files are distinct on canvas: +the latter is what you will see when the file is placed in a module, while the former is what is shown in the file structure. + +You can place a file in as many modules as you wish by specifying the modules in the `meta.json` file. +The key `module` has a value which is a list of names of modules in the Canvas course. +If no module with the specified name exists, a module will be created to house the file. + +The `destination` key specifies where in the file structure you would like the file to be placed. + +Note that while a file cannot be simultaneously placed in multiple file structure locations using `meta.json`, if `meta.json` is updated, +the file will not automatically be deleted from any previous location unless that instance is specifically deleted. + + +Example +======= + +If the `meta.json` file looks like: + +.. code-block:: + + { + "type":"file", + "title":"Syllabus", + "filename":"F24_Math100_syllabus.pdf", + "modules":["Course Information", "Week 1"], + "destination": "course_info/syllabus_schedule" + } + +then the file `F24_Math100_syllabus.pdf` will be put into two modules: `Course Information` and `Week 1`. +Within these two modules, its title will appear to students as `Syllabus`. The file will be located in `course_info/syllabus_schedule`, +which will be created if it did not already exist. + + + diff --git a/test/_styles/custom/Ruapehu_and_Ngauruhoe.jpg b/test/_styles/custom/Ruapehu_and_Ngauruhoe.jpg new file mode 100644 index 0000000..f24a886 Binary files /dev/null and b/test/_styles/custom/Ruapehu_and_Ngauruhoe.jpg differ diff --git a/test/_styles/custom/footer.html b/test/_styles/custom/footer.html new file mode 100644 index 0000000..12fd014 --- /dev/null +++ b/test/_styles/custom/footer.html @@ -0,0 +1,4 @@ + + + + diff --git a/test/_styles/custom/footer.md b/test/_styles/custom/footer.md new file mode 100644 index 0000000..6e916b5 --- /dev/null +++ b/test/_styles/custom/footer.md @@ -0,0 +1,4 @@ +--- + +Header image credit: Jeremy Visser, CC BY-SA 4.0 , via Wikimedia Commons. + diff --git a/test/_styles/custom/header.html b/test/_styles/custom/header.html new file mode 100644 index 0000000..df618fc --- /dev/null +++ b/test/_styles/custom/header.html @@ -0,0 +1,7 @@ + + + + +
+ + diff --git a/test/_styles/custom/header.md b/test/_styles/custom/header.md new file mode 100644 index 0000000..72d4842 --- /dev/null +++ b/test/_styles/custom/header.md @@ -0,0 +1,4 @@ + +![This is a photo of Mount Ruapehu and Mount Ngauruhoe looking west from the Desert Road in Tongariro National Park (New Zealand) in January 2015.]($PATHTOMD2CANVASSTYLEFILE/Ruapehu_and_Ngauruhoe.jpg) + +--- diff --git a/test/a_file.file/meta.json b/test/a_file.file/meta.json index 1b6bd2d..fac4a8f 100644 --- a/test/a_file.file/meta.json +++ b/test/a_file.file/meta.json @@ -2,6 +2,6 @@ "type":"file", "title":"automatically uploaded file: ds150_course_logo.pdf", "filename":"ds150_course_logo.pdf", - "modules":["Automatically Added Test Module", "Another automatically added test module"], + "modules":["Automatically Added Test Module", "Another automatically added test module", "module created by file upload"], "destination": "automatically_uploaded_files/a_subfolder" } \ No newline at end of file diff --git a/test/test_file.py b/test/test_file.py index 0c8fd84..66b221a 100644 --- a/test/test_file.py +++ b/test/test_file.py @@ -4,6 +4,8 @@ import markdown2canvas as mc import pytest +import json +import datetime @pytest.fixture(scope='class') def course(): @@ -16,12 +18,28 @@ def course(): yield canvas.get_course(test_course_id) @pytest.fixture(scope='class') -def content(course): +def content(): import os folder = 'a_file.file' yield mc.File(folder) +#the following gives all instances of that file, if it lives in multiple locations +@pytest.fixture(scope='class') +def file_list(course): + yield course.get_files(search_term = 'ds150_course_logo.pdf') + +#gives the current instance, based on the destination in meta.json +@pytest.fixture(scope='class') +def current_file(course, file_list): + with open('a_file.file/meta.json', "r", encoding="utf-8") as f: + folder_name = json.loads(f.read())['destination'] + for instance in file_list: + if instance.folder_id == course.get_folders(search_term=folder_name)[0].id: + yield instance + + + class TestFile(): @@ -33,9 +51,24 @@ def test_meta(self, content): def test_can_publish(self, course, content): content.publish(course,overwrite=True) assert content.is_already_uploaded(course) + + def test_in_modules(self, course, content): + content.publish(course,overwrite=True) for m in content.metadata['modules']: assert content.is_in_module(course, m) + module_test = course.get_modules(search_term = m)[0] + assert module_test.get_module_items(search_term = 'ds150')[0].title == 'automatically uploaded file: ds150_course_logo.pdf' + #tests that it ends up in the folder you specified this time (it can simultaneously be in another folder if you put it there previously) + def test_in_folder(self, course, content, file_list, current_file): + content.publish(course,overwrite=True) + with open('a_file.file/meta.json', "r", encoding="utf-8") as f: + folder_name = json.loads(f.read())['destination'] + folder_list=[] + for instance in file_list: + folder_list.append(instance.folder_id) + assert course.get_folders(search_term=folder_name)[0].id in folder_list + assert current_file.folder_id == course.get_folders(search_term=folder_name)[0].id def test_already_online_raises(self, course, content): # publish once, forcefully. @@ -46,5 +79,14 @@ def test_already_online_raises(self, course, content): content.publish(course,overwrite=False) # default is False + def test_attributes(self, course, content, current_file): + content.publish(course,overwrite=True) + assert current_file.filename == 'ds150_course_logo.pdf' + assert current_file.modified_at_date.day == datetime.date.today().day + + + + + diff --git a/test/test_page.py b/test/test_page.py index 58bcdaa..bf776f5 100644 --- a/test/test_page.py +++ b/test/test_page.py @@ -43,9 +43,7 @@ def test_already_online_raises(self, course, page_has_local_images): def test_doesnt_find_deleted(self, course, page_has_local_images): name = page_has_local_images.name - page_has_local_images.publish(course,overwrite=True) - assert mc.is_page_already_uploaded(name,course) f = mc.find_page_in_course(name,course) f.delete() assert not mc.is_page_already_uploaded(name,course) @@ -54,3 +52,8 @@ def test_can_find_published(self, course, page_has_local_images): page_has_local_images.publish(course,overwrite=True) assert mc.is_page_already_uploaded(page_has_local_images.name,course) + def test_content(self, course): + content = course.get_pages(search_term='Test Has Local Images')[0].show_latest_revision().body + assert 'testing source including images' in content + assert 'alt="A menagerie of Herwig Hauser surfaces"' in content + diff --git a/test/test_page_in_module.py b/test/test_page_in_module.py index 80543de..9e50f94 100644 --- a/test/test_page_in_module.py +++ b/test/test_page_in_module.py @@ -31,11 +31,9 @@ def destination_modules(page_plain_text_in_a_module): yield page.metadata['modules'] -#self._delete_test_modules() - -def _delete_test_modules(self): - for m in self.destination_modules: - mc.delete_module(m, self.course, even_if_exists=True) +def _delete_test_modules(course, destination_modules): + for m in destination_modules: + mc.delete_module(m, course, even_if_doesnt_exist=True) @@ -64,12 +62,12 @@ def test_can_make_modules(self, course, destination_modules): def test_can_delete_modules(self, course, destination_modules): - + _delete_test_modules(course, destination_modules) for m in destination_modules: mc.create_or_get_module(m,course) for m in destination_modules: - mc.delete_module(m, course, even_if_exists=False) + mc.delete_module(m, course, even_if_doesnt_exist=False) diff --git a/test/test_replacements.py b/test/test_replacements.py index d8c7381..c471da0 100644 --- a/test/test_replacements.py +++ b/test/test_replacements.py @@ -2,8 +2,11 @@ sys.path.insert(0,'../') import markdown2canvas as mc +import json + import pytest + @pytest.fixture(scope='class') def course(): import os @@ -17,21 +20,66 @@ def course(): @pytest.fixture(scope='class') -def page_using_defaults(course): +def page_using_defaults(): import os folder = 'uses_replacements_default' yield mc.Page(folder) + @pytest.fixture(scope='class') -def page_using_custom(course): +def page_using_custom(): import os folder = 'uses_replacements_custom' yield mc.Page(folder) +@pytest.fixture(scope='class') +def default_filename(): + with open('_course_metadata/defaults.json', "r", encoding="utf-8") as f: + defaults = f.read() + yield json.loads(defaults)['replacements'] + +@pytest.fixture(scope='class') +def replacements_default(default_filename): + with open(default_filename, "r", encoding="utf-8") as f: + yield f.read() + +@pytest.fixture(scope='class') +def uses_defaults_source(): + with open('uses_replacements_default/source.md', "r", encoding="utf-8") as f: + yield f.read() + + +@pytest.fixture(scope='class') +def html_using_defaults(course): + a = course.get_pages(search_term = 'Test replacements using default replacements file')[0] + rev = a.show_latest_revision() + yield rev.body + + +@pytest.fixture(scope='class') +def replacements_custom(): + with open('_course_metadata/replacements2.json', "r", encoding="utf-8") as f: + yield f.read() + +@pytest.fixture(scope='class') +def uses_custom_source(): + with open('uses_replacements_custom/source.md', "r", encoding="utf-8") as f: + yield f.read() + +@pytest.fixture(scope='class') +def html_using_custom(course): + a = course.get_pages(search_term = 'Test replacements with custom replacements file')[0] + rev = a.show_latest_revision() + yield rev.body + + + + + class TestPage(): def test_can_publish(self, course, page_using_defaults, page_using_custom): @@ -39,7 +87,57 @@ def test_can_publish(self, course, page_using_defaults, page_using_custom): page_using_custom.publish(course,overwrite=True) - ##Removed a " as e_info" after the def in the following... doesn't seem to have hurt it? + def test_get_default_replacements_name(self): + path = mc.get_default_replacements_name() + assert path == '_course_metadata/replacements.json' + + + def test_removed_default(self, html_using_defaults, replacements_default, uses_defaults_source): + replacements_dict_default = json.loads(replacements_default) + for key in replacements_dict_default: + if key in uses_defaults_source: + assert key not in html_using_defaults + #Want to add something about the new thing being in the html + #assert replacements_dict_default[key] in html_using_defaults + + def test_replaced_default(self, html_using_defaults): + #default replacements that should translate seamlessly + assert 'with this text' in html_using_defaults + assert 'destination_without_spaces' in html_using_defaults + #check specific video options + assert '560' in html_using_defaults + assert '315' in html_using_defaults + assert 'https://www.youtube.com/embed/dQw4w9WgXcQ?si=BqTm4nbZOLTHaxnz' in html_using_defaults + assert 'YouTube video player' in html_using_defaults + assert 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share' in html_using_defaults + assert 'allowfullscreen' in html_using_defaults + + def test_removed_custom(self, html_using_custom, uses_custom_source, replacements_custom): + replacements_dict_custom = json.loads(replacements_custom) + for key in replacements_dict_custom: + if key in uses_custom_source: + assert key not in html_using_custom + assert replacements_dict_custom[key] in html_using_custom + + def test_replaced_custom(self, html_using_custom): + #custom replacements that should translate seamlessly + assert 'target custom replacement without space' in html_using_custom + assert 'target custom replacement from nospace' in html_using_custom + + def test_incorrect_replacement_custom(self, html_using_custom): + #First check that none of the default replacements show up in the custom replacements file + assert 'with this text' not in html_using_custom + assert 'destination_without_spaces' not in html_using_custom + assert 'https://www.youtube.com/embed/dQw4w9WgXcQ?si=BqTm4nbZOLTHaxnz' not in html_using_custom + + + def test_incorrect_replacement_default(self, html_using_defaults): + #First check that none of the default replacements show up in the custom replacements file + assert 'target custom replacement without space' not in html_using_defaults + assert 'target custom replacement from nospace' not in html_using_defaults + + + def test_missing_replacements(self): # constructing a page with a replacements file that doesn't exist should raise with pytest.raises(FileNotFoundError): diff --git a/test/test_style.py b/test/test_style.py index a1b5e22..b210bab 100644 --- a/test/test_style.py +++ b/test/test_style.py @@ -17,36 +17,55 @@ def course(): yield canvas.get_course(test_course_id) @pytest.fixture(scope='class') -def page_uses_droplets_via_style(course): +def page_uses_droplets_via_style_generic(): import os folder = 'uses_droplets_via_style' yield mc.Page(folder) +@pytest.fixture(scope='class') +def page_uses_droplets_via_style_custom(): + import os + folder = 'uses_droplets_via_style_custom' + + yield mc.Page(folder) + + +@pytest.fixture(scope='class') +def page_contents_generic(course): + a = course.get_pages(search_term = 'Test Uses Droplets via Style')[1] + rev = a.show_latest_revision() + yield rev.body + +@pytest.fixture(scope='class') +def page_contents_custom(course): + a = course.get_pages(search_term = 'Test Uses Droplets via Style Custom')[0] + rev = a.show_latest_revision() + yield rev.body class TestStyle(): - def test_meta(self, page_uses_droplets_via_style): - assert page_uses_droplets_via_style.name == 'Test Uses Droplets via Style' + def test_meta(self, page_uses_droplets_via_style_generic,page_uses_droplets_via_style_custom): + assert page_uses_droplets_via_style_generic.name == 'Test Uses Droplets via Style' + assert page_uses_droplets_via_style_custom.name == 'Test Uses Droplets via Style Custom' - def test_can_publish(self, course, page_uses_droplets_via_style): - page_uses_droplets_via_style.publish(course,overwrite=True) + def test_can_publish(self, course, page_uses_droplets_via_style_generic,page_uses_droplets_via_style_custom): + page_uses_droplets_via_style_generic.publish(course,overwrite=True) + page_uses_droplets_via_style_custom.publish(course,overwrite=True) - def test_already_online_raises(self, course, page_uses_droplets_via_style): + def test_already_online_raises(self, course,page_uses_droplets_via_style_custom): # publish once, forcefully. - page_uses_droplets_via_style.publish(course,overwrite=True) - + page_uses_droplets_via_style_custom.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise with pytest.raises(mc.exception.AlreadyExists): - page_uses_droplets_via_style.publish(course,overwrite=False) # default is False - + page_uses_droplets_via_style_custom.publish(course,overwrite=False) # default is False - def test_doesnt_find_deleted(self, course, page_uses_droplets_via_style): - name = page_uses_droplets_via_style.name + def test_doesnt_find_deleted(self, course, page_uses_droplets_via_style_generic): + name = page_uses_droplets_via_style_generic.name - page_uses_droplets_via_style.publish(course,overwrite=True) + page_uses_droplets_via_style_generic.publish(course,overwrite=True) assert mc.is_page_already_uploaded(name,course) f = mc.find_page_in_course(name,course) f.delete() @@ -54,8 +73,21 @@ def test_doesnt_find_deleted(self, course, page_uses_droplets_via_style): assert not mc.is_page_already_uploaded(name,course) - def test_can_find_published(self, course, page_uses_droplets_via_style): - page_uses_droplets_via_style.publish(course,overwrite=True) - assert mc.is_page_already_uploaded(page_uses_droplets_via_style.name,course) + def test_can_find_published(self, course, page_uses_droplets_via_style_generic): + page_uses_droplets_via_style_generic.publish(course,overwrite=True) + assert mc.is_page_already_uploaded(page_uses_droplets_via_style_generic.name,course) + def test_default_style_implemented(course, page_contents_generic): + assert 'Markdown header content here' in page_contents_generic + assert 'Header image credit: Medoffer, CC BY-SA 4.0' in page_contents_generic + assert 'This is a photo of a natural heritage site in Ukraine, id: 59-247-5004.' in page_contents_generic + + def test_custom_style_implemented(course, page_contents_custom): + assert 'Header image credit: Jeremy Visser, CC BY-SA 4.0' in page_contents_custom + assert 'This is a photo of Mount Ruapehu and Mount Ngauruhoe looking west from the Desert Road in Tongariro National Park (New Zealand) in January 2015.' in page_contents_custom + + def test_incorrect_style_used(course, page_contents_generic, page_contents_custom): + assert 'Header image credit: Medoffer, CC BY-SA 4.0' not in page_contents_custom + assert 'Markdown header content here' not in page_contents_custom + assert 'Header image credit: Jeremy Visser, CC BY-SA 4.0' not in page_contents_generic diff --git a/test/uses_droplets_via_style_custom/hauser_menagerie.jpg b/test/uses_droplets_via_style_custom/hauser_menagerie.jpg new file mode 100644 index 0000000..fc1dc27 Binary files /dev/null and b/test/uses_droplets_via_style_custom/hauser_menagerie.jpg differ diff --git a/test/uses_droplets_via_style_custom/meta.json b/test/uses_droplets_via_style_custom/meta.json new file mode 100644 index 0000000..f2319d2 --- /dev/null +++ b/test/uses_droplets_via_style_custom/meta.json @@ -0,0 +1,6 @@ +{ + "type": "page", + "name": "Test Uses Droplets via Style Custom", + "style": "_styles/custom", + "modules":["Automatically Added Module"] +} diff --git a/test/uses_droplets_via_style_custom/source.md b/test/uses_droplets_via_style_custom/source.md new file mode 100644 index 0000000..4e139b1 --- /dev/null +++ b/test/uses_droplets_via_style_custom/source.md @@ -0,0 +1,39 @@ + +# Testing upload of pages using Droplets + + +## an image using droplets + +
+ A menagerie of Herwig Hauser surfaces +
+ + +
+ +## ^^^ a horizontal rule + +## Code + +
+import numpy as np
+x,y= np.linspace(-1,1,20),np.linspace(-1,1,21)
+X,Y = np.meshgrid(x,y)
+
+ +## testing callouts + +
+

Heading

+

Fred and George, who had been spying on the Slytherin team, had seen for themselves the speed of those new Nimbus Two Thousand and Ones.

+
+ + + +## a table should be below + +| header 1 | header 2 | +| --- | --- | +| val 1 | val 2 | +| arst | axzcv | +