From 9b85ee978e3188f2356fac23869a9c82bf69b140 Mon Sep 17 00:00:00 2001 From: silviana amethyst <1388063+ofloveandhate@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:43:33 -0500 Subject: [PATCH] refactor completed? namespace is so much cleaner. added module level docstrings docstrings for most functions added all unit tests passed --- docs/{classes.rst => canvas_objects.rst} | 16 +- docs/exceptions.rst | 6 +- docs/free_functions.rst | 29 +- docs/markdown2canvas.rst | 2 +- docs/requirements.txt | 3 +- markdown2canvas/__init__.py | 65 ++-- markdown2canvas/base_classes/__init__.py | 235 ------------- .../__init__.py => canvas2markdown.py} | 108 ++++-- .../__init__.py => canvas_objects.py} | 327 +++++++++++++++++- .../course_interaction_functions.py | 280 +++++++++++++++ markdown2canvas/exception.py | 45 +++ markdown2canvas/exception/__init__.py | 29 -- markdown2canvas/logging.py | 46 +++ markdown2canvas/setup_functions.py | 68 ++++ markdown2canvas/{tool/__init__.py => tool.py} | 10 +- .../__init__.py => translation_functions.py} | 309 ++--------------- test/course_id.py | 2 +- test/test_assignment.py | 2 +- test/test_download_pages_to_markdown.py | 4 +- test/test_droplets.py | 2 +- test/test_file.py | 2 +- test/test_link.py | 2 +- test/test_link_to_local_file.py | 2 +- test/test_page.py | 2 +- test/test_page_in_module.py | 2 +- test/test_style.py | 2 +- 26 files changed, 958 insertions(+), 642 deletions(-) rename docs/{classes.rst => canvas_objects.rst} (54%) delete mode 100644 markdown2canvas/base_classes/__init__.py rename markdown2canvas/{canvas2markdown/__init__.py => canvas2markdown.py} (62%) rename markdown2canvas/{classes/__init__.py => canvas_objects.py} (69%) create mode 100644 markdown2canvas/course_interaction_functions.py create mode 100644 markdown2canvas/exception.py delete mode 100644 markdown2canvas/exception/__init__.py create mode 100644 markdown2canvas/logging.py create mode 100644 markdown2canvas/setup_functions.py rename markdown2canvas/{tool/__init__.py => tool.py} (89%) rename markdown2canvas/{free_functions/__init__.py => translation_functions.py} (57%) diff --git a/docs/classes.rst b/docs/canvas_objects.rst similarity index 54% rename from docs/classes.rst rename to docs/canvas_objects.rst index 7d056da..6529ff4 100644 --- a/docs/classes.rst +++ b/docs/canvas_objects.rst @@ -1,5 +1,5 @@ -Concrete Classes -------------------- +Concrete Classes for Canvas Objects +-------------------------------------- @@ -12,6 +12,14 @@ Concrete Classes :members: :undoc-members: +.. autoclass:: markdown2canvas.Image + :members: + :undoc-members: + + +.. autoclass:: markdown2canvas.Link + :members: + :undoc-members: .. autoclass:: markdown2canvas.File @@ -32,11 +40,11 @@ Concrete Classes Base Classes -------------- -.. autoclass:: markdown2canvas.CanvasObject +.. autoclass:: markdown2canvas.canvas_objects.CanvasObject :members: :undoc-members: -.. autoclass:: markdown2canvas.Document +.. autoclass:: markdown2canvas.canvas_objects.Document :members: :undoc-members: \ No newline at end of file diff --git a/docs/exceptions.rst b/docs/exceptions.rst index 9279079..1cb36b9 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -1,14 +1,14 @@ Exceptions ------------- -.. autoclass:: markdown2canvas.AlreadyExists +.. autoclass:: markdown2canvas.exception.AlreadyExists :members: :undoc-members: -.. autoclass:: markdown2canvas.SetupError +.. autoclass:: markdown2canvas.exception.SetupError :members: :undoc-members: -.. autoclass:: markdown2canvas.DoesntExist +.. autoclass:: markdown2canvas.exception.DoesntExist :members: :undoc-members: \ No newline at end of file diff --git a/docs/free_functions.rst b/docs/free_functions.rst index e2a292e..0c0c3de 100644 --- a/docs/free_functions.rst +++ b/docs/free_functions.rst @@ -1,3 +1,30 @@ Free functions ------------------- +===================== + + +Setup functions +------------------- + +.. automodule:: markdown2canvas.setup_functions + :members: + + + +Functions for interacting with a course on Canvas +--------------------------------------------------- + +.. automodule:: markdown2canvas.course_interaction_functions + :members: + + +Functions markdown2canvas uses to translate from markdown to Canvas-html +-------------------------------------------------------------------------- + +.. automodule:: markdown2canvas.translation_functions + :members: + +.. autofunction:: markdown2canvas.canvas_objects.find_local_images + +.. autofunction:: markdown2canvas.canvas_objects.find_local_files + diff --git a/docs/markdown2canvas.rst b/docs/markdown2canvas.rst index 6b89fd0..49b26ba 100644 --- a/docs/markdown2canvas.rst +++ b/docs/markdown2canvas.rst @@ -12,7 +12,7 @@ Python libraries used: `canvasapi` .. toctree:: :maxdepth: 2 - classes + canvas_objects exceptions free_functions diff --git a/docs/requirements.txt b/docs/requirements.txt index ddc2a8f..c1f96fb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ canvasapi -Pygments \ No newline at end of file +Pygments +markdown \ No newline at end of file diff --git a/markdown2canvas/__init__.py b/markdown2canvas/__init__.py index 6280eb7..9e53524 100644 --- a/markdown2canvas/__init__.py +++ b/markdown2canvas/__init__.py @@ -1,60 +1,59 @@ -import canvasapi -import os.path as path -import os -import requests +""" +`markdown2canvas`, a library for containerizing and publishing Canvas content. +Containerization of content is via filesystem folders with a `meta.json` file specifying type of content. Some content types like Assignment and Page use `source.md`, while others like File and Image are just a `meta.json` plus the files. +Publishing content is via the `.publish` member function for the canvas object, like -import logging +``` +my_assignment.publish(course) +``` -# logging.basicConfig(encoding='utf-8') +Documentation may be found at the GitHub pages for this library. Use it. -import datetime -today = datetime.datetime.today().strftime("%Y-%m-%d") -log_level=logging.DEBUG +A more complete example might be -log_dir = path.join(path.normpath(os.getcwd()), '_logs') +``` +import markdown2canvas as mc +canvas_url = "https://uweau.instructure.com/" # 🎯 REPLACE WITH YOUR URL -if not path.exists(log_dir): - os.mkdir(log_dir) +# get the course. +course_id = 705022 # 🎯 REPLACE WITH YOUR NUMBER!!!!!!!!!!!!!!!!! +course = canvas.get_course(course_id) -log_filename = path.join(log_dir, f'markdown2canvas_{today}.log') +# make the API object. this is from the `canvasapi` library, NOT something in `markdown2canvas`. +canvas = mc.make_canvas_api_obj(url=canvas_url) +my_assignment = mc.Assignent('path_to_assignment") -log_encoding = 'utf-8' +# finally, publish +my_assignment.publish(course) +``` +""" -root_logger = logging.getLogger() -root_logger.setLevel(log_level) -handler = logging.FileHandler(log_filename, 'a', log_encoding) -root_logger.addHandler(handler) +# the root-level file for `markdown2canvas` -logging.debug(f'starting logging at {datetime.datetime.now()}') +__version__ = '0.' +__author__ = 'silviana amethyst, Mckenzie West, Allison Beemer' -logging.debug(f'reducing logging level of `requests` to WARNING') -logging.getLogger('canvasapi.requester').setLevel(logging.WARNING) -logging.getLogger('requests').setLevel(logging.WARNING) +import markdown2canvas.logging +import markdown2canvas.exception +from markdown2canvas.setup_functions import * +import markdown2canvas.translation_functions - -from markdown2canvas.exception import AlreadyExists, SetupError, DoesntExist - - -from markdown2canvas.free_functions import * - +from markdown2canvas.course_interaction_functions import * ################## classes -from markdown2canvas.base_classes import CanvasObject, Document -from markdown2canvas.classes import Page, Assignment, Image, BareFile, Link, File - - - +from markdown2canvas.canvas_objects import CanvasObject, Document, Page, Assignment, Image, File, BareFile, Link +import markdown2canvas.canvas2markdown import markdown2canvas.tool diff --git a/markdown2canvas/base_classes/__init__.py b/markdown2canvas/base_classes/__init__.py deleted file mode 100644 index cc04458..0000000 --- a/markdown2canvas/base_classes/__init__.py +++ /dev/null @@ -1,235 +0,0 @@ - -import os.path as path -import os - -import canvasapi -from markdown2canvas.free_functions import * - -class CanvasObject(object): - """ - A base class for wrapping canvas objects. - """ - - def __init__(self,canvas_obj=None): - - super(object, self).__init__() - - self.canvas_obj = canvas_obj - - - - -class Document(CanvasObject): - """ - A base class which handles common pieces of interface for things like Pages and Assignments - - This type is abstract. Assignments and Pages both derive from this. - - At least two files are required in the folder for a Document: - - 1. `meta.json` - 2. `source.md` - - You may have additional files in the folder for a Document, such as images and files to include in the content on Canvas. This library will automatically upload those for you! - """ - - def __init__(self,folder,course=None): - """ - Construct a Document. - Reads the meta.json file and source.md files - from the specified folder. - """ - - super(Document,self).__init__(folder) - import json, os - from os.path import join - - self.folder = folder - - # try to open, and see if the meta and source files exist. - # if not, raise - self.metaname = path.join(folder,'meta.json') - with open(self.metaname,'r',encoding='utf-8') as f: - self.metadata = json.load(f) - - self.sourcename = path.join(folder,'source.md') - - - # variables populated from the metadata. should these even exist? IDK - self.name = None - self.style_path = None - self.replacements_path = None - - # populate the above variables from the meta.json file - self._set_from_metadata() - - - # these internally-used variables are used to carry state between functions - self._local_images = None - self._local_files = None - self._translated_html = None - - - def _set_from_metadata(self): - """ - this function is called during `__init__`. - """ - - self.name = self.metadata['name'] - - if 'modules' in self.metadata: - self.modules = self.metadata['modules'] - else: - self.modules = [] - - if 'indent' in self.metadata: - self.indent = self.metadata['indent'] - else: - self.indent = 0 - - if 'style' in self.metadata: - self.style_path = find_in_containing_directory_path(self.metadata['style']) - else: - self.style_path = get_default_style_name() # could be None if doesn't exist - if self.style_path: - self.style_path = find_in_containing_directory_path(self.style_path) - - if 'replacements' in self.metadata: - self.replacements_path = find_in_containing_directory_path(self.metadata['replacements']) - else: - self.replacements_path = get_default_replacements_name() # could be None if doesn't exist - if self.replacements_path: - self.replacements_path = find_in_containing_directory_path(self.replacements_path) - - - - - def translate_to_html(self,course): - """ - populates the internal variables with the results of translating from markdown to html. - - This step requires the `course` since this library allows for referencing of content already on canvas (or to be later published on Canvas) - - The main result of translation is held in self._translated_html. The local content (on YOUR computer, NOT Canvas) is parsed out and held in `self._local_images` and `self._local_files`. - - * This function does NOT make content appear on Canvas. - * It DOES leave behind a temporary file: `{folder}/styled_source.md`. Be sure to add `*/styled_source.md` to your .gitignore for your course! - """ - from os.path import join - - if self.style_path: - outname = join(self.folder,"styled_source.md") - apply_style_markdown(self.sourcename, self.style_path, outname) - - translated_html_without_hf = markdown2html(outname,course, self.replacements_path) - - self._translated_html = apply_style_html(translated_html_without_hf, self.style_path, outname) - else: - self._translated_html = markdown2html(self.sourcename,course, self.replacements_path) - - - self._local_images = find_local_images(self._translated_html) - self._local_files = find_local_files(self._translated_html) - - - - - - def publish_linked_content_and_adjust_html(self,course,overwrite=False): - """ - this function should be called *after* `translate_to_html`, since it requires the internal variables that other function populates - - the result of this function is written to `folder/result.html` - - * This function does NOT make content appear on Canvas. - * It DOES leave behind a temporary file: `{folder}/result.html`. Be sure to add `*/result.html` to your .gitignore for your course! - """ - - # first, publish the local images. - for im in self._local_images.values(): - im.publish(course,'images', overwrite=overwrite) - - for file in self._local_files.values(): - file.publish(course,'automatically_uploaded_files', overwrite=overwrite) - - - # then, deal with the urls - self._translated_html = adjust_html_for_images(self._translated_html, self._local_images, course.id) - self._translated_html = adjust_html_for_files(self._translated_html, self._local_files, course.id) - - save_location = path.join(self.folder,'result.html') - with open(save_location,'w',encoding='utf-8') as result: - result.write(self._translated_html) - - - - - - - - - - def _construct_dict_of_props(self): - """ - construct a dictionary of properties, such that it can be used to `edit` a canvas object. - """ - d = {} - return d - - - def ensure_in_modules(self, course): - """ - makes sure this item is listed in the Module on Canvas. If it's not, it's added to the bottom. There's not currently any way to control order. - - If the item doesn't already exist, this function will raise. Be sure to actually publish the content first. - """ - - if not self.canvas_obj: - raise DoesntExist(f"trying to make sure an object is in its modules, but this item ({self.name}) doesn't exist on canvas yet. publish it first.") - - for module_name in self.modules: - module = create_or_get_module(module_name, course) - - if not self.is_in_module(module_name, course): - - if self.metadata['type'] == 'page': - content_id = self.canvas_obj.page_id - elif self.metadata['type'] == 'assignment': - content_id = self.canvas_obj.id - - - module.create_module_item(module_item={'type':self.metadata['type'], 'content_id':content_id, 'indent':self.indent}) - - - def is_in_module(self, module_name, course): - """ - checks whether this content is an item in the listed module, where `module_name` is a string. It's case sensitive and exact. - - passthrough raise if the module doesn't exist - """ - - module = get_module(module_name,course) - - for item in module.get_module_items(): - - if item.type=='Page': - if self.metadata['type']=='page': - - if course.get_page(item.page_url).title == self.name: - return True - - else: - continue - - - if item.type=='Assignment': - if self.metadata['type']=='assignment': - - if course.get_assignment(assignment=item.content_id).name == self.name: - return True - else: - continue - - return False - - diff --git a/markdown2canvas/canvas2markdown/__init__.py b/markdown2canvas/canvas2markdown.py similarity index 62% rename from markdown2canvas/canvas2markdown/__init__.py rename to markdown2canvas/canvas2markdown.py index d3e6131..d1c53fb 100644 --- a/markdown2canvas/canvas2markdown/__init__.py +++ b/markdown2canvas/canvas2markdown.py @@ -1,11 +1,84 @@ +''' +Functions for grabbing non-containerized content from Canvas and saving it to disk. Useful for making a markdown2canvas repo from an existing course. + +The two main functions you should use are `download_pages` and `download_assignments`. + +The resulting folder will have name equal to the name of the content on Canvas, for better or for worse. + +I can see some situations where folder names are invalid -- feel free to improve this functionality. PR's welcome. + +''' + +__all__ = [ + 'download_pages','download_assignments' + 'page2markdown','assignment2markdown'] + + + import canvasapi +import os.path as path + +import logging +logger = logging.getLogger() + + + + +def download_pages(destination, course, even_if_exists=False, name_filter=None): + """ + downloads the regular pages from a course, saving them + into a markdown2canvas compatible format. that is, as + a folder with markdown source and json metadata. + + You can provide a predicate `name_filter` to filter on the name of the content! The function should return True/False. + + The flag `even_if_exists` is to overwrite the local content. If `even_if_exists` is True, the remote content will be written to disk EVEN IF IT ALREADY EXISTED LOCALLY. Thus, this may involve data loss. Use version control. + """ + + if name_filter is None: + name_filter = lambda x: True + + logger.info(f'downloading all pages from course {course.name}, saving to folder {destination}') + pages = course.get_pages() + for p in pages: + if name_filter(p.show_latest_revision().title): + page2markdown(destination,p,even_if_exists) + + +def download_assignments(destination, course, even_if_exists=False, name_filter=None): + """ + downloads the assignments from a course, saving them + into a markdown2canvas compatible format. that is, as + a folder with markdown source and json metadata. + + `destination` is the path you want to write the content to. This function will make sub-folders of `destination`. + + You can provide a predicate `name_filter` to filter on the name of the content! The function should return True/False. + + The flag `even_if_exists` is to overwrite the local content. If `even_if_exists` is True, the remote content will be written to disk EVEN IF IT ALREADY EXISTED LOCALLY. Thus, this may involve data loss. Use version control. + """ + + if name_filter is None: + name_filter = lambda x: True + + logger.info(f'downloading all pages from course {course.name}, saving to folder {destination}') + assignments = course.get_assignments() + for a in assignments: + if name_filter(a.name): + assignment2markdown(destination,a,even_if_exists) + + + def page2markdown(destination, page, even_if_exists=False): """ takes a Page from Canvas, and saves it to a folder inside `destination` into a markdown2canvas compatible format. - the folder is automatically named, at your own peril. + the folder is automatically named, at your own peril. Colons are removed, and spaces are replaced by underscores. + + The flag `even_if_exists` is to overwrite the local content. If `even_if_exists` is True, the remote content will be written to disk EVEN IF IT ALREADY EXISTED LOCALLY. Thus, this may involve data loss. Use version control. + """ import os @@ -47,21 +120,6 @@ def page2markdown(destination, page, even_if_exists=False): -def download_pages(destination, course, even_if_exists=False, name_filter=None): - """ - downloads the regular pages from a course, saving them - into a markdown2canvas compatible format. that is, as - a folder with markdown source and json metadata. - """ - - if name_filter is None: - name_filter = lambda x: True - - logger.info(f'downloading all pages from course {course.name}, saving to folder {destination}') - pages = course.get_pages() - for p in pages: - if name_filter(p.show_latest_revision().title): - page2markdown(destination,p,even_if_exists) def assignment2markdown(destination, assignment, even_if_exists=False): @@ -69,7 +127,9 @@ def assignment2markdown(destination, assignment, even_if_exists=False): takes a Page from Canvas, and saves it to a folder inside `destination` into a markdown2canvas compatible format. - the folder is automatically named, at your own peril. + the folder is automatically named, at your own peril. Colons are removed, and spaces are replaced by underscores. + + The flag `even_if_exists` is to overwrite the local content. If `even_if_exists` is True, the remote content will be written to disk EVEN IF IT ALREADY EXISTED LOCALLY. Thus, this may involve data loss. Use version control. """ import os @@ -106,18 +166,6 @@ def assignment2markdown(destination, assignment, even_if_exists=False): import json json.dump(d, file) -def download_assignments(destination, course, even_if_exists=False, name_filter=None): - """ - downloads the regular pages from a course, saving them - into a markdown2canvas compatible format. that is, as - a folder with markdown source and json metadata. - """ - if name_filter is None: - name_filter = lambda x: True - logger.info(f'downloading all pages from course {course.name}, saving to folder {destination}') - assignments = course.get_assignments() - for a in assignments: - if name_filter(a.name): - assignment2markdown(destination,a,even_if_exists) + diff --git a/markdown2canvas/classes/__init__.py b/markdown2canvas/canvas_objects.py similarity index 69% rename from markdown2canvas/classes/__init__.py rename to markdown2canvas/canvas_objects.py index 99f8371..1b4494c 100644 --- a/markdown2canvas/classes/__init__.py +++ b/markdown2canvas/canvas_objects.py @@ -1,10 +1,329 @@ +""" +Main content types for markdown2canvas + +Page and Assignment both require meta.json and source.md + +Image, File, BareFile each require meta.json and the file they containerize. Importantly, the file does NOT need to be contained in the folder!!! I'm trying to make your life easy. + +Link requires meta.json and that's it +""" + + +__all__ = [ + 'Page', + 'Assignment', + 'Image', + 'BareFile', + 'Link', + 'File' +] -import canvasapi import os.path as path import os -from markdown2canvas.base_classes import Document, CanvasObject -from markdown2canvas.free_functions import * +import canvasapi +from markdown2canvas.logging import * +from markdown2canvas.setup_functions import * +from markdown2canvas.translation_functions import * +from markdown2canvas.course_interaction_functions import * +from markdown2canvas.exception import AlreadyExists, SetupError, DoesntExist + + + + + +def find_local_images(html): + """ + constructs a map of local url's : Images + """ + from bs4 import BeautifulSoup + + soup = BeautifulSoup(html,features="lxml") + + local_images = {} + + all_imgs = soup.findAll("img") + + if all_imgs: + for img in all_imgs: + src = img["src"] + if src[:7] not in ['https:/','http://']: + local_images[src] = Image(path.abspath(src)) + + return local_images + + + +def find_local_files(html): + """ + constructs a list of BareFiles, so that they can later be replaced with a url to a canvas thing + """ + from bs4 import BeautifulSoup + + soup = BeautifulSoup(html,features="lxml") + + local_files = {} + + all_links = soup.findAll("a") + + if all_links: + for file in all_links: + href = file["href"] + if path.exists(path.abspath(href)): + local_files[href] = BareFile(path.abspath(href)) + + return local_files + + +class CanvasObject(object): + """ + A base class for wrapping canvas objects. + """ + + def __init__(self,canvas_obj=None): + + super(object, self).__init__() + + self.canvas_obj = canvas_obj + + + + +class Document(CanvasObject): + """ + A base class which handles common pieces of interface for things like Pages and Assignments + + This type is abstract. Assignments and Pages both derive from this. + + At least two files are required in the folder for a Document: + + 1. `meta.json` + 2. `source.md` + + You may have additional files in the folder for a Document, such as images and files to include in the content on Canvas. This library will automatically upload those for you! + """ + + def __init__(self,folder,course=None): + """ + Construct a Document. + Reads the meta.json file and source.md files + from the specified folder. + """ + + super(Document,self).__init__(folder) + import json, os + from os.path import join + + self.folder = folder + + # try to open, and see if the meta and source files exist. + # if not, raise + self.metaname = path.join(folder,'meta.json') + with open(self.metaname,'r',encoding='utf-8') as f: + self.metadata = json.load(f) + + self.sourcename = path.join(folder,'source.md') + + + # variables populated from the metadata. should these even exist? IDK + self.name = None + self.style_path = None + self.replacements_path = None + + # populate the above variables from the meta.json file + self._set_from_metadata() + + + # these internally-used variables are used to carry state between functions + self._local_images = None + self._local_files = None + self._translated_html = None + + + def _set_from_metadata(self): + """ + this function is called during `__init__`. + """ + + self.name = self.metadata['name'] + + if 'modules' in self.metadata: + self.modules = self.metadata['modules'] + else: + self.modules = [] + + if 'indent' in self.metadata: + self.indent = self.metadata['indent'] + else: + self.indent = 0 + + if 'style' in self.metadata: + self.style_path = find_in_containing_directory_path(self.metadata['style']) + else: + self.style_path = get_default_style_name() # could be None if doesn't exist + if self.style_path: + self.style_path = find_in_containing_directory_path(self.style_path) + + if 'replacements' in self.metadata: + self.replacements_path = find_in_containing_directory_path(self.metadata['replacements']) + else: + self.replacements_path = get_default_replacements_name() # could be None if doesn't exist + if self.replacements_path: + self.replacements_path = find_in_containing_directory_path(self.replacements_path) + + + + + def translate_to_html(self,course): + """ + populates the internal variables with the results of translating from markdown to html. + + This step requires the `course` since this library allows for referencing of content already on canvas (or to be later published on Canvas) + + The main result of translation is held in self._translated_html. The local content (on YOUR computer, NOT Canvas) is parsed out and held in `self._local_images` and `self._local_files`. + + * This function does NOT make content appear on Canvas. + * It DOES leave behind a temporary file: `{folder}/styled_source.md`. Be sure to add `*/styled_source.md` to your .gitignore for your course! + """ + from os.path import join + + if self.style_path: + outname = join(self.folder,"styled_source.md") + apply_style_markdown(self.sourcename, self.style_path, outname) + + translated_html_without_hf = markdown2html(outname,course, self.replacements_path) + + self._translated_html = apply_style_html(translated_html_without_hf, self.style_path, outname) + else: + self._translated_html = markdown2html(self.sourcename,course, self.replacements_path) + + + self._local_images = find_local_images(self._translated_html) + self._local_files = find_local_files(self._translated_html) + + + + + + def publish_linked_content_and_adjust_html(self,course,overwrite=False): + """ + this function should be called *after* `translate_to_html`, since it requires the internal variables that other function populates + + the result of this function is written to `folder/result.html` + + * This function does NOT make content appear on Canvas. + * It DOES leave behind a temporary file: `{folder}/result.html`. Be sure to add `*/result.html` to your .gitignore for your course! + """ + + # first, publish the local images. + for im in self._local_images.values(): + im.publish(course,'images', overwrite=overwrite) + + for file in self._local_files.values(): + file.publish(course,'automatically_uploaded_files', overwrite=overwrite) + + + # then, deal with the urls + self._translated_html = adjust_html_for_images(self._translated_html, self._local_images, course.id) + self._translated_html = adjust_html_for_files(self._translated_html, self._local_files, course.id) + + save_location = path.join(self.folder,'result.html') + with open(save_location,'w',encoding='utf-8') as result: + result.write(self._translated_html) + + + + + + + + + + def _construct_dict_of_props(self): + """ + construct a dictionary of properties, such that it can be used to `edit` a canvas object. + """ + d = {} + return d + + + def ensure_in_modules(self, course): + """ + makes sure this item is listed in the Module on Canvas. If it's not, it's added to the bottom. There's not currently any way to control order. + + If the item doesn't already exist, this function will raise. Be sure to actually publish the content first. + """ + + if not self.canvas_obj: + raise DoesntExist(f"trying to make sure an object is in its modules, but this item ({self.name}) doesn't exist on canvas yet. publish it first.") + + for module_name in self.modules: + module = create_or_get_module(module_name, course) + + if not self.is_in_module(module_name, course): + + if self.metadata['type'] == 'page': + content_id = self.canvas_obj.page_id + elif self.metadata['type'] == 'assignment': + content_id = self.canvas_obj.id + + + module.create_module_item(module_item={'type':self.metadata['type'], 'content_id':content_id, 'indent':self.indent}) + + + def is_in_module(self, module_name, course): + """ + checks whether this content is an item in the listed module, where `module_name` is a string. It's case sensitive and exact. + + passthrough raise if the module doesn't exist + """ + + module = get_module(module_name,course) + + for item in module.get_module_items(): + + if item.type=='Page': + if self.metadata['type']=='page': + + if course.get_page(item.page_url).title == self.name: + return True + + else: + continue + + + if item.type=='Assignment': + if self.metadata['type']=='assignment': + + if course.get_assignment(assignment=item.content_id).name == self.name: + return True + else: + continue + + return False + + + + + + + + + + + + + + + + + + + + + + + class Page(Document): """ @@ -649,5 +968,3 @@ def is_already_uploaded(self,course, require_same_path=True): return None - - diff --git a/markdown2canvas/course_interaction_functions.py b/markdown2canvas/course_interaction_functions.py new file mode 100644 index 0000000..1d9d771 --- /dev/null +++ b/markdown2canvas/course_interaction_functions.py @@ -0,0 +1,280 @@ +""" +Functions for making or getting things in Canvas, mostly by names-as-strings. + +Note that `canvasapi` mostly uses numeric identifiers to get things. This annoyed me and so I wrote these functions. + +These functions do NOT require containerized content, so these functions are probably useful even without a containerized course using markdown2canvas. +""" + +__all__ = [ + 'is_file_already_uploaded', + 'find_file_in_course', + 'is_page_already_uploaded', + 'find_page_in_course', + 'is_assignment_already_uploaded', + 'find_assignment_in_course', + 'get_root_folder', + 'get_assignment_group_id', + 'create_or_get_assignment', + 'create_or_get_page', + 'create_or_get_module', + 'get_module', + 'get_subfolder_named', + 'delete_module' + ] + + +from markdown2canvas.exception import * +import canvasapi + +import os.path as path + + + + + + +def is_file_already_uploaded(filename,course): + """ + returns a boolean, true if there's a file of `filename` already in `course`. + + This function wants the full path to the file. + + See also `find_file_in_course` + """ + return ( not find_file_in_course(filename,course) is None ) + + + + +def find_file_in_course(filename,course): + """ + Checks to see of the file at `filename` is already in the "files" part of `course`. + + It tests filename and size as reported on disk. If it finds a match, then it's up. + + This function wants the full path to the file. + + Note that `canvasapi` does NOT differentiate + between files in different "folders" on Canvas, + so if you have multiple files of the same name, + this will find the first one that matches both name and size. + """ + import os + + base = path.split(filename)[1] + + files = course.get_files() + for f in files: + if f.filename==base and f.size == path.getsize(filename): + return f + + return None + + + + + +def is_page_already_uploaded(name,course): + """ + returns a boolean indicating whether a page of the given `name` is already in the `course`. + """ + return ( not find_page_in_course(name,course) is None ) + + +def find_page_in_course(name,course): + """ + Checks to see if there's already a page named `name` as part of `course`. + + tests merely based on the name. assumes assignments are uniquely named. + """ + + import os + pages = course.get_pages() + for p in pages: + if p.title == name: + return p + + return None + + + +def is_assignment_already_uploaded(name,course): + """ + returns a boolean indicating whether an assignment of the given `name` is already in the `course`. + """ + return ( not find_assignment_in_course(name,course) is None ) + + +def find_assignment_in_course(name,course): + """ + Checks to see if there's already an assignment named `name` as part of `course`. + + Tests merely based on the name. assumes assingments are uniquely named. + """ + import os + assignments = course.get_assignments() + for a in assignments: + + if a.name == name: + return a + + return None + + + + + +def get_root_folder(course): + """ + gets the Folder object at root level in your course. + """ + + for f in course.get_folders(): + if f.full_name == 'course files': + return f + + + + + + +def get_assignment_group_id(assignment_group_name, course, create_if_necessary=False): + """ + gets the ID number of an assignment group from its name-as-string. + + `create_if_necessary`: There are two distinct behaviours available: + + False: [default] If such a group doesn't exist, this will raise. + True: Will make such an assignment group if it doesn't exist. + + Gods, I hope the preceding description made you feel like "well duh" because my names were that spot-on. If not, let's grab a beer together and talk about it. If you read this, you're amazing, and I'm glad you're using my software. I'm trying so hard to leave positive legacy! + """ + + existing_groups = course.get_assignment_groups() + + if not isinstance(assignment_group_name,str): + raise RuntimeError(f'assignment_group_name must be a string, but I got {assignment_group_name} of type {type(assignment_group_name)}') + + + for g in existing_groups: + if g.name == assignment_group_name: + return g.id + + + + if create_if_necessary: + msg = f'making new assignment group `{assignment_group_name}`' + logger.info(msg) + + group = course.create_assignment_group(name=assignment_group_name) + group.edit(name=assignment_group_name) # this feels stupid. didn't i just request its name be this? + + return group.id + else: + raise DoesntExist(f'cannot get assignment group id because an assignment group of name {assignment_group_name} does not already exist, and `create_if_necessary` is set to False') + + + + + +def create_or_get_assignment(name, course, even_if_exists = False): + """ + gets the `canvasapi.Assignment`. Can tell it to make the assignment if it didn't exist. + """ + + if is_assignment_already_uploaded(name,course): + if even_if_exists: + return find_assignment_in_course(name,course) + else: + raise AlreadyExists(f"assignment {name} already exists") + else: + # make new assignment of name in course. + return course.create_assignment(assignment={'name':name}) + + + +def create_or_get_page(name, course, even_if_exists): + """ + gets the `canvasapi.Page`. Can tell it to make the page if it didn't exist. + """ + + if is_page_already_uploaded(name,course): + + if even_if_exists: + return find_page_in_course(name,course) + else: + raise AlreadyExists(f"page {name} already exists") + else: + # make new assignment of name in course. + result = course.create_page(wiki_page={'body':"empty page",'title':name}) + return result + + + + +def create_or_get_module(module_name, course): + """ + gets the `canvasapi.Module`. Can tell it to make the module if it didn't exist. + """ + + try: + return get_module(module_name, course) + except DoesntExist as e: + return course.create_module(module={'name':module_name}) + + + + +def get_module(module_name, course): + """ + returns + * canvasapi.Module if such a module exists, + * raises if not + """ + modules = course.get_modules() + + for m in modules: + if m.name == module_name: + return m + + raise DoesntExist(f"tried to get module {module_name}, but it doesn't exist in the course") + + +def get_subfolder_named(folder, subfolder_name): + """ + gets the `canvasapi.Folder` with matching name. + + this is likely broken if subfolder has a / in its name, / gets converted to something else by Canvas. don't use / in subfolder names, that's not allowed + + raises if doesn't exist. + """ + + assert '/' not in subfolder_name, "this is likely broken if subfolder has a / in its name, / gets converted to something else by Canvas. don't use / in subfolder names, that's not allowed" + + current_subfolders = folder.get_folders() + for f in current_subfolders: + if f.name == subfolder_name: + return f + + raise DoesntExist(f'a subfolder of {folder.name} named {subfolder_name} does not currently exist') + + +def delete_module(module_name, course, even_if_exists): + ''' + Deletes a module by name-as-string. + ''' + + if even_if_exists: + try: + m = get_module(module_name, course) + m.delete() + except DoesntExist as e: + return + + else: + # this path is expected to raise if the module doesn't exist + m = get_module(module_name, course) + m.delete() + + diff --git a/markdown2canvas/exception.py b/markdown2canvas/exception.py new file mode 100644 index 0000000..ef6a246 --- /dev/null +++ b/markdown2canvas/exception.py @@ -0,0 +1,45 @@ +''' +Exception types emitted by markdown2canvas +''' + +__all__ = [ + 'AlreadyExists', + 'SetupError', + 'DoesntExist' +] + +class AlreadyExists(Exception): + """ + Used to indicate that you're trying to do a thing cautiously, and the thing already existed on Canvas. + """ + + def __init__(self, message, errors=""): + super().__init__(message) + + self.errors = errors + +class SetupError(Exception): + """ + Used to indicate that markdown2canvas couldn't get off the ground, or there's something else wrong that's not content-related but meta or config. + """ + + def __init__(self, message, errors=""): + super().__init__(message) + + self.errors = errors + + + + +class DoesntExist(Exception): + """ + Used when getting a thing, but it doesn't exist. + """ + + def __init__(self, message, errors=""): + super().__init__(message) + + self.errors = errors + + + diff --git a/markdown2canvas/exception/__init__.py b/markdown2canvas/exception/__init__.py deleted file mode 100644 index 209c61b..0000000 --- a/markdown2canvas/exception/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -class AlreadyExists(Exception): - - def __init__(self, message, errors=""): - # Call the base class constructor with the parameters it needs - super().__init__(message) - - self.errors = errors - -class SetupError(Exception): - - def __init__(self, message, errors=""): - # Call the base class constructor with the parameters it needs - super().__init__(message) - - self.errors = errors - - - - -class DoesntExist(Exception): - """ - Used when getting a thing, but it doesn't exist - """ - - def __init__(self, message, errors=""): - # Call the base class constructor with the parameters it needs - super().__init__(message) - - self.errors = errors \ No newline at end of file diff --git a/markdown2canvas/logging.py b/markdown2canvas/logging.py new file mode 100644 index 0000000..ff7403a --- /dev/null +++ b/markdown2canvas/logging.py @@ -0,0 +1,46 @@ +''' +Logging utilities. Uses the built-in `logging` library. This part could probably be improved to allow the user to set their own levels or turn on/off logging more easily. +''' + +__all__ = [ + 'today', 'log_dir', 'logger', 'file_handler' + ] + +import os.path as path +import os + +import logging + +# logging.basicConfig(encoding='utf-8') + +import datetime +today = datetime.datetime.today().strftime("%Y-%m-%d") + +log_level=logging.DEBUG + +log_dir = path.join(path.normpath(os.getcwd()), '_logs') + +if not path.exists(log_dir): + os.mkdir(log_dir) + +log_filename = path.join(log_dir, f'markdown2canvas_{today}.log') + + +log_encoding = 'utf-8' + +# make a logger object. we'll getLogger in the other files as needed. + +logger = logging.getLogger() # make a root-level logger using the defaulted options. see https://stackoverflow.com/questions/50714316/how-to-use-logging-getlogger-name-in-multiple-modules + +# adjust the logger for THIS module +logger.setLevel(log_level) + +# make a file handler and attach +file_handler = logging.FileHandler(log_filename, 'a', log_encoding) +logger.addHandler(file_handler) + +# a few messages to start +logging.debug(f'starting logging at {datetime.datetime.now()}') +logging.debug(f'reducing logging level of `requests` and `canvasapi` to WARNING') +logging.getLogger('canvasapi.requester').setLevel(logging.WARNING) +logging.getLogger('requests').setLevel(logging.WARNING) \ No newline at end of file diff --git a/markdown2canvas/setup_functions.py b/markdown2canvas/setup_functions.py new file mode 100644 index 0000000..cf442e2 --- /dev/null +++ b/markdown2canvas/setup_functions.py @@ -0,0 +1,68 @@ +''' +Functions for making a `canvasapi.Canvas` object with which to work + +Uses environment variables to let you specify things. +''' + +__all__ = ['get_canvas_key_url', 'make_canvas_api_obj'] + + +import os.path as path +import os + +import logging +logger = logging.getLogger(__name__) + +import canvasapi + + + + + +def get_canvas_key_url(): + """ + reads a file using an environment variable, namely the file specified in `CANVAS_CREDENTIAL_FILE`. + + We need the + + * API_KEY + * API_URL + + variables from that file. + """ + from os import environ + + cred_loc = environ.get('CANVAS_CREDENTIAL_FILE') + if cred_loc is None: + raise SetupError('`get_canvas_key_url()` needs an environment variable `CANVAS_CREDENTIAL_FILE`, containing the full path of the file containing your Canvas API_KEY, *including the file name*') + + # yes, this is scary. it was also low-hanging fruit, and doing it another way was going to be too much work + with open(path.join(cred_loc),encoding='utf-8') as cred_file: + exec(cred_file.read(),locals()) + + if isinstance(locals()['API_KEY'], str): + logger.info(f'using canvas with API_KEY as defined in {cred_loc}') + else: + raise SetupError(f'failing to use canvas. Make sure that file {cred_loc} contains a line of code defining a string variable `API_KEY="keyhere"`') + + return locals()['API_KEY'],locals()['API_URL'] + + +def make_canvas_api_obj(url=None): + """ + - reads the key from a python file, path to which must be in environment variable CANVAS_CREDENTIAL_FILE. + - optionally, pass in a url to use, in case you don't want the default one you put in your CANVAS_CREDENTIAL_FILE. + """ + + key, default_url = get_canvas_key_url() + + if not url: + url = default_url + + return canvasapi.Canvas(url, key) + + + + + + diff --git a/markdown2canvas/tool/__init__.py b/markdown2canvas/tool.py similarity index 89% rename from markdown2canvas/tool/__init__.py rename to markdown2canvas/tool.py index e39909f..2d3af9e 100644 --- a/markdown2canvas/tool/__init__.py +++ b/markdown2canvas/tool.py @@ -1,4 +1,8 @@ -import markdown2canvas as mc +""" +Provides a base class from which to derive when writing tools that will interact with `markdown2canvas` or `canvasapi`. +""" + +__all__ = ['Tool'] class Tool(object): @@ -59,7 +63,9 @@ def __init__(self, config_name = 'config.json'): def _read_config(self, config_name): - + """ + reads `config_name` (config.json by default), and unpacks `course_id`. Stores the de-serialized json file in self.config. + """ import json with open(config_name,'r') as f: config = json.load(f) diff --git a/markdown2canvas/free_functions/__init__.py b/markdown2canvas/translation_functions.py similarity index 57% rename from markdown2canvas/free_functions/__init__.py rename to markdown2canvas/translation_functions.py index 45ccd04..a515fae 100644 --- a/markdown2canvas/free_functions/__init__.py +++ b/markdown2canvas/translation_functions.py @@ -1,136 +1,27 @@ -import os.path as path -import os - -import logging -logger = logging.getLogger(__name__) - -import canvasapi - -def is_file_already_uploaded(filename,course): - """ - returns a boolean, true if there's a file of `filename` already in `course`. - - This function wants the full path to the file. - """ - return ( not find_file_in_course(filename,course) is None ) - - - - -def find_file_in_course(filename,course): - """ - Checks to see of the file at `filename` is already in the "files" part of `course`. - - It tests filename and size as reported on disk. If it finds a match, then it's up. - - This function wants the full path to the file. - """ - import os - - base = path.split(filename)[1] - - files = course.get_files() - for f in files: - if f.filename==base and f.size == path.getsize(filename): - return f - - return None - - - - - -def is_page_already_uploaded(name,course): - """ - returns a boolean indicating whether a page of the given `name` is already in the `course`. - """ - return ( not find_page_in_course(name,course) is None ) - - -def find_page_in_course(name,course): - """ - Checks to see if there's already a page named `name` as part of `course`. - - tests merely based on the name. assumes assignments are uniquely named. - """ - import os - pages = course.get_pages() - for p in pages: - if p.title == name: - return p - - return None - - - -def is_assignment_already_uploaded(name,course): - """ - returns a boolean indicating whether an assignment of the given `name` is already in the `course`. - """ - return ( not find_assignment_in_course(name,course) is None ) - - -def find_assignment_in_course(name,course): - """ - Checks to see if there's already an assignment named `name` as part of `course`. - - tests merely based on the name. assumes assingments are uniquely named. - """ - import os - assignments = course.get_assignments() - for a in assignments: - - if a.name == name: - return a - - return None - - - - -def get_canvas_key_url(): - """ - reads a file using an environment variable, namely the file specified in `CANVAS_CREDENTIAL_FILE`. - - We need the - - * API_KEY - * API_URL - - variables from that file. - """ - from os import environ - - cred_loc = environ.get('CANVAS_CREDENTIAL_FILE') - if cred_loc is None: - raise SetupError('`get_canvas_key_url()` needs an environment variable `CANVAS_CREDENTIAL_FILE`, containing the full path of the file containing your Canvas API_KEY, *including the file name*') - - # yes, this is scary. it was also low-hanging fruit, and doing it another way was going to be too much work - with open(path.join(cred_loc),encoding='utf-8') as cred_file: - exec(cred_file.read(),locals()) - - if isinstance(locals()['API_KEY'], str): - logger.info(f'using canvas with API_KEY as defined in {cred_loc}') - else: - raise SetupError(f'failing to use canvas. Make sure that file {cred_loc} contains a line of code defining a string variable `API_KEY="keyhere"`') - - return locals()['API_KEY'],locals()['API_URL'] - - -def make_canvas_api_obj(url=None): - """ - - reads the key from a python file, path to which must be in environment variable CANVAS_CREDENTIAL_FILE. - - optionally, pass in a url to use, in case you don't want the default one you put in your CANVAS_CREDENTIAL_FILE. - """ - - key, default_url = get_canvas_key_url() - - if not url: - url = default_url - - return canvasapi.Canvas(url, key) +""" +Functions for translating markdown to html, +putting headers/footers around content, +and manipulating links to link to or embed images and files on Canvas. +""" + +__all__ = [ + 'generate_course_link', + 'find_in_containing_directory_path', + 'preprocess_replacements', + 'preprocess_markdown_images', + 'get_default_property', + 'get_default_style_name', + 'get_default_replacements_name', + 'apply_style_markdown', + 'apply_style_html', + 'markdown2html', + 'adjust_html_for_images', + 'adjust_html_for_files' +] +import os.path as path +import os def generate_course_link(type,name,all_of_type,courseid=None): ''' @@ -367,26 +258,6 @@ def markdown2html(filename, course, replacements_path): -def find_local_images(html): - """ - constructs a map of local url's : Images - """ - from bs4 import BeautifulSoup - - soup = BeautifulSoup(html,features="lxml") - - local_images = {} - - all_imgs = soup.findAll("img") - - if all_imgs: - for img in all_imgs: - src = img["src"] - if src[:7] not in ['https:/','http://']: - local_images[src] = Image(path.abspath(src)) - - return local_images - @@ -424,26 +295,6 @@ def adjust_html_for_images(html, published_images, courseid): -def find_local_files(html): - """ - constructs a list of BareFiles, so that they can later be replaced with a url to a canvas thing - """ - from bs4 import BeautifulSoup - - soup = BeautifulSoup(html,features="lxml") - - local_files = {} - - all_links = soup.findAll("a") - - if all_links: - for file in all_links: - href = file["href"] - if path.exists(path.abspath(href)): - local_files[href] = BareFile(path.abspath(href)) - - return local_files - def adjust_html_for_files(html, published_files, courseid): @@ -475,123 +326,7 @@ def adjust_html_for_files(html, published_files, courseid): -def get_root_folder(course): - for f in course.get_folders(): - if f.full_name == 'course files': - return f - - - - - - - - -def get_assignment_group_id(assignment_group_name, course, create_if_necessary=False): - existing_groups = course.get_assignment_groups() - if not isinstance(assignment_group_name,str): - raise RuntimeError(f'assignment_group_name must be a string, but I got {assignment_group_name} of type {type(assignment_group_name)}') - - for g in existing_groups: - if g.name == assignment_group_name: - return g.id - - - - if create_if_necessary: - msg = f'making new assignment group `{assignment_group_name}`' - logger.info(msg) - - group = course.create_assignment_group(name=assignment_group_name) - group.edit(name=assignment_group_name) # this feels stupid. didn't i just request its name be this? - - return group.id - else: - raise DoesntExist(f'cannot get assignment group id because an assignment group of name {assignment_group_name} does not already exist, and `create_if_necessary` is set to False') - - - - - -def create_or_get_assignment(name, course, even_if_exists = False): - - if is_assignment_already_uploaded(name,course): - if even_if_exists: - return find_assignment_in_course(name,course) - else: - raise AlreadyExists(f"assignment {name} already exists") - else: - # make new assignment of name in course. - return course.create_assignment(assignment={'name':name}) - - - -def create_or_get_page(name, course, even_if_exists): - if is_page_already_uploaded(name,course): - - if even_if_exists: - return find_page_in_course(name,course) - else: - raise AlreadyExists(f"page {name} already exists") - else: - # make new assignment of name in course. - result = course.create_page(wiki_page={'body':"empty page",'title':name}) - return result - - - - -def create_or_get_module(module_name, course): - - try: - return get_module(module_name, course) - except DoesntExist as e: - return course.create_module(module={'name':module_name}) - - - - -def get_module(module_name, course): - """ - returns - * Module if such a module exists, - * raises if not - """ - modules = course.get_modules() - - for m in modules: - if m.name == module_name: - return m - - raise DoesntExist(f"tried to get module {module_name}, but it doesn't exist in the course") - - -def get_subfolder_named(folder, subfolder_name): - - assert '/' not in subfolder_name, "this is likely broken if subfolder has a / in its name, / gets converted to something else by Canvas. don't use / in subfolder names, that's not allowed" - - current_subfolders = folder.get_folders() - for f in current_subfolders: - if f.name == subfolder_name: - return f - - raise DoesntExist(f'a subfolder of {folder.name} named {subfolder_name} does not currently exist') - - -def delete_module(module_name, course, even_if_exists): - - if even_if_exists: - try: - m = get_module(module_name, course) - m.delete() - except DoesntExist as e: - return - - else: - # this path is expected to raise if the module doesn't exist - m = get_module(module_name, course) - m.delete() diff --git a/test/course_id.py b/test/course_id.py index 9586ff5..7c6fdd2 100644 --- a/test/course_id.py +++ b/test/course_id.py @@ -1 +1 @@ -test_course_id = 537006 +test_course_id = 705022 diff --git a/test/test_assignment.py b/test/test_assignment.py index 41c8e83..5b616b3 100644 --- a/test/test_assignment.py +++ b/test/test_assignment.py @@ -64,7 +64,7 @@ def test_already_online_raises(self, course, assignment): assignment.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): assignment.publish(course,overwrite=False) # default is False def test_doesnt_find_deleted(self, course, assignment): diff --git a/test/test_download_pages_to_markdown.py b/test/test_download_pages_to_markdown.py index cd68c8c..1d3ccd5 100644 --- a/test/test_download_pages_to_markdown.py +++ b/test/test_download_pages_to_markdown.py @@ -32,7 +32,7 @@ def test_aaa_can_download_all_pages(self): if os.path.exists(destination): shutil.rmtree(destination) - mc.download_pages(destination, self.course, even_if_exists=False) + mc.canvas2markdown.download_pages(destination, self.course, even_if_exists=False) def test_aaa_can_download_some_pages(self): import os, shutil @@ -41,7 +41,7 @@ def test_aaa_can_download_some_pages(self): shutil.rmtree(destination) my_filter = lambda title: 'test' in title.lower() - mc.download_pages(destination, self.course, even_if_exists=False, name_filter=my_filter) + mc.canvas2markdown.download_pages(destination, self.course, even_if_exists=False, name_filter=my_filter) if __name__ == '__main__': diff --git a/test/test_droplets.py b/test/test_droplets.py index 818cef3..ca69255 100644 --- a/test/test_droplets.py +++ b/test/test_droplets.py @@ -48,7 +48,7 @@ def test_already_online_raises(self, course, page): page.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): page.publish(course,overwrite=False) # default is False diff --git a/test/test_file.py b/test/test_file.py index e32a7da..0c8fd84 100644 --- a/test/test_file.py +++ b/test/test_file.py @@ -42,7 +42,7 @@ def test_already_online_raises(self, course, content): content.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): content.publish(course,overwrite=False) # default is False diff --git a/test/test_link.py b/test/test_link.py index 05058b0..49b4073 100644 --- a/test/test_link.py +++ b/test/test_link.py @@ -51,7 +51,7 @@ def test_already_online_raises(self,course,link): link.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): link.publish(course,overwrite=False) # default is False diff --git a/test/test_link_to_local_file.py b/test/test_link_to_local_file.py index d910e99..4dbb790 100644 --- a/test/test_link_to_local_file.py +++ b/test/test_link_to_local_file.py @@ -49,7 +49,7 @@ def test_already_online_raises(self, course, page): page.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): page.publish(course,overwrite=False) # default is False diff --git a/test/test_page.py b/test/test_page.py index 19fa8b7..58bcdaa 100644 --- a/test/test_page.py +++ b/test/test_page.py @@ -38,7 +38,7 @@ def test_already_online_raises(self, course, page_has_local_images): page_has_local_images.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): page_has_local_images.publish(course,overwrite=False) # default is False def test_doesnt_find_deleted(self, course, page_has_local_images): diff --git a/test/test_page_in_module.py b/test/test_page_in_module.py index d48152b..80543de 100644 --- a/test/test_page_in_module.py +++ b/test/test_page_in_module.py @@ -54,7 +54,7 @@ def test_already_online_raises(self, course, page_plain_text_in_a_module): page_plain_text_in_a_module.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): page_plain_text_in_a_module.publish(course,overwrite=False) # default is False diff --git a/test/test_style.py b/test/test_style.py index 8ad8bd0..a1b5e22 100644 --- a/test/test_style.py +++ b/test/test_style.py @@ -39,7 +39,7 @@ def test_already_online_raises(self, course, page_uses_droplets_via_style): page_uses_droplets_via_style.publish(course,overwrite=True) # the second publish, with overwrite=False, should raise - with pytest.raises(mc.AlreadyExists): + with pytest.raises(mc.exception.AlreadyExists): page_uses_droplets_via_style.publish(course,overwrite=False) # default is False