diff --git a/README.md b/README.md index 88431dd..0805106 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,11 @@ The output will be a text file in GIFT format with the same name as the input on ### Questions -Questions are specified through another *YAML* file. The first parameter, `pictures base directory`, refers to the base directory that will be created in the remote host for all your embedded images. It is meant to separate different question banks (so that you can have, e.g., directories `quiz 1` and `quiz 2`). The remaining of the file is a **list of categories**, and inside each one there is a **list of questions**. Hopefully, the format is clear from either the name of the settings and/or its companion comments. You are probably better of taking a look at the [provided example](bank.yaml). +Questions are specified through another *YAML* file. The first parameter, `pictures base directory`, refers to the base directory that will be created in the remote host for all your embedded images. It is meant to separate different question banks (so that you can have, e.g., directories `quiz 1` and `quiz 2`). The remaining of the file is a **list of categories**, and inside each one there is a **list of questions**. Hopefully, the format is clear from either the name of the settings and/or its companion comments. You are probably better off taking a look at the [provided example](bank.yaml). ### Example -If you run the program inside the `gift-wrapper` directory as is, it will process the sample `bank.yaml` which includes a `.tex`, a `.svg` and some mathematical formulas, and will generate a `bank.gift` file which you can import from Moodle (choosing the GIFT format when asked). +If you run the program inside the `gift-wrapper` directory as is, it will process the sample `bank.yaml` which includes a `.tex`, a `.svg` and some mathematical formulas, and will generate a `bank.gift.txt` file which you can import from Moodle (choosing the GIFT format when asked). ## Including images @@ -73,6 +73,10 @@ In any case, you just need to write the path to the file inside the text of the Images (*svg*s) are then copied to a remote host, and properly linked in the output GIFT file. +### Browser compatibility + +It seems (it has been reported) not every browser properly handles svg images (maybe other types too) embedded in a question as an URL. My experience so far is both *Firefox* and *Chromium* (at the time of writing this) work just fine. + ### Do I really need pdf2svg? Only if you want automatic conversion from `.tex` (passing through `.pdf`) to `.svg`, i.e., only if you embed a `.tex` somewhere. diff --git a/question.py b/question.py index 95a22a3..3e0f37c 100644 --- a/question.py +++ b/question.py @@ -14,7 +14,7 @@ class HtmlQuestion(metaclass=abc.ABCMeta): Abstract class implementing an html-based question. """ - def __init__(self, name: str, statement: str, images_settings: dict, feedback: Optional[str] = None): + def __init__(self, name: str, statement: str, images_settings: dict, history: dict, feedback: Optional[str] = None): """ Initializer. @@ -33,6 +33,7 @@ def __init__(self, name: str, statement: str, images_settings: dict, feedback: O self.name = name self.statement = statement.rstrip() self.feedback = feedback + self.history = history assert ('width' in images_settings) and ('height' in images_settings),\ '"width" and/or "height" missing in "image_settings"' @@ -115,9 +116,10 @@ class Numerical(HtmlQuestion): """ def __init__( - self, name: str, statement: str, images_settings: dict, solution: dict, feedback: Optional[str] = None): + self, name: str, statement: str, images_settings: dict, history: dict, solution: dict, + feedback: Optional[str] = None): - super().__init__(name, statement, images_settings, feedback) + super().__init__(name, statement, images_settings, history, feedback) assert ('value' in solution), '"value" missing in "solution"' @@ -141,9 +143,11 @@ class MultipleChoice(HtmlQuestion): Class implementing a multiple-choice question. """ - def __init__(self, name: str, statement: str, images_settings: dict, answers: dict, feedback: Optional[str] = None): + def __init__( + self, name: str, statement: str, images_settings: dict, history: dict, answers: dict, + feedback: Optional[str] = None): - super().__init__(name, statement, images_settings, feedback) + super().__init__(name, statement, images_settings, history, feedback) self.answers = answers @@ -199,8 +203,6 @@ def transform_files( # all the matching files in the given text files = re.findall(pattern, text) - # breakpoint() - # every one of them... for file in files: @@ -217,11 +219,25 @@ def __init__(self, decorated: Union[HtmlQuestion, QuestionDecorator]): super().__init__(decorated) + def process_match(f): + + # if this file has not been already compiled-converted... + if f not in self.history['already compiled']: + + # ...it is... + image.pdf_to_svg(image.compile_tex(f)) + + # ...and a note is made of it + self.history['already compiled'].add(f) + + # else: + # + # print(f'{f} already compiled-converted...') + # a new pre-processing function is attached to the corresponding list # (the "\1" in `replacement` refers to matches in `pattern`) self.pre_processing_functions.append(functools.partial( - self.transform_files, pattern='(\S+)\.tex', process_match=lambda x: image.pdf_to_svg(image.compile_tex(x)), - replacement=r'\1.svg')) + self.transform_files, pattern='(\S+)\.tex', process_match=process_match, replacement=r'\1.svg')) class SvgToHttp(QuestionDecorator): @@ -244,8 +260,22 @@ def replacement_function(m: re.Match) -> str: return public_url + pictures_base_directory + '/' + file.as_posix() + def process_match(f): + + # if this file has not been already transferred... + if f not in self.history['already transferred']: + + # ...it is... + connection.copy(f, remote_directory=remote_subdirectory / pathlib.Path(f).parent) + + # ...and a note is made of it + self.history['already transferred'].add(f) + + # else: + # + # print(f'{f} already transferred...') + # a new pre-processing function is attached to the corresponding list self.pre_processing_functions.append(functools.partial( self.transform_files, pattern='(? None: + + self.already_copied = set() + @staticmethod def is_active(): return False - @staticmethod - def copy(source: Union[str, pathlib.Path], remote_directory: str): + def copy(self, source: Union[str, pathlib.Path], remote_directory: str): + + source = pathlib.Path(source) + + if source.as_posix() not in self.already_copied: + + print( + f'{colors.info}you *should* copy {colors.reset}{source}{colors.info} to' + f' {colors.reset}{remote_directory}') - print(f'{colors.info}you *should* copy {colors.reset}{source}{colors.info} to {colors.reset}{remote_directory}') + self.already_copied.add(source.as_posix()) @staticmethod def make_directory_at(new: str, at: str): - print(f'{colors.info}you *should* make directory {colors.reset}{new}{colors.info} at {colors.reset}{at}') + pass + # print(f'{colors.info}you *should* make directory {colors.reset}{new}{colors.info} at {colors.reset}{at}') diff --git a/wrap.py b/wrap.py index 42d139c..2a5d111 100755 --- a/wrap.py +++ b/wrap.py @@ -61,9 +61,12 @@ # ...an actual connection with the requested host is opened connection = remote.Connection(parameters['images hosting']['copy']['host'], **parameters['images hosting']['ssh']) -# output file has the same name as the input with the ".gift" suffix +# output file has the same name as the input with the ".gift.txt" suffix output_file = input_file.with_suffix('.gift.txt') +# to keep track of files already compiled/transferred +history = {'already compiled': set(), 'already transferred': set()} + with open(output_file, 'w') as f: # for every category... @@ -86,7 +89,7 @@ # for every question in the category... for q in tqdm.tqdm(cat['questions'], desc='question', leave=False): - # that class that will be instantiated for this particular question + # the class that will be instantiated for this particular question question_class = getattr(question, q['class']) # if field `images_settings` is not present... @@ -98,7 +101,10 @@ # the class is removed from the dictionary so that it doesn't get passed to the initializer del q['class'] - # question is instantiated and decorated + # "history" is passed + q['history'] = history + + # question is instantiated and "decorated" q = question.SvgToHttp( question.TexToSvg(question_class(**q)), connection, parameters['images hosting']['copy']['public filesystem root'], @@ -106,4 +112,4 @@ f.write(f'{q.gift}\n\n') -print(f'file "{output_file}" created') \ No newline at end of file +print(f'{colors.info}file "{colors.reset}{output_file}{colors.info}" created') \ No newline at end of file