diff --git a/apluslms_roman/cli.py b/apluslms_roman/cli.py index b00f57d..34aed66 100644 --- a/apluslms_roman/cli.py +++ b/apluslms_roman/cli.py @@ -2,12 +2,17 @@ from os import getcwd from os.path import abspath, expanduser, expandvars from sys import exit +import logging from . import __version__ from. builder import Engine from .backends.docker import DockerBackend from .configuration import CourseConfigError, CourseConfig +from .validator import Validator +LOG_LEVELS = [logging.WARNING, logging.INFO, logging.DEBUG] + +logger = logging.getLogger(__name__) def main(): parser = argparse.ArgumentParser(description='Course material builder') @@ -16,8 +21,15 @@ def main(): parser.add_argument('--version', action='store_true', help='Print version info') + parser.add_argument("-v", "--verbose", dest="verbose_count", + action="count", default=0, + help="increases log verbosity for each occurence.") args = parser.parse_args() + logging.basicConfig( + level=LOG_LEVELS[min(args.verbose_count, 2)], + #format='%(name)s (%(levelname)s): %(message)s' + ) if args.version: print("Roman {}\n".format(__version__)) @@ -53,10 +65,21 @@ def main(): print("Invalid course config: {}".format(e)) exit(1) + # create validator (and validate config) + logging.info(" >> Validation step:") + validator = Validator(config) + # validation_result = validator.validate(location of the file here, "config", 1) + # if validation_result is False: + # logging.warning( "Validation unsuccessful. Terminating.") + # exit(0) + logging.info(" >> All files validated succesfully") + + # build course builder = engine.create_builder(config) result = builder.build() print(result) + logging.shutdown() exit(result.code or 0) diff --git a/apluslms_roman/schemas/config_v1_0.yaml b/apluslms_roman/schemas/config_v1_0.yaml new file mode 100644 index 0000000..7443a27 --- /dev/null +++ b/apluslms_roman/schemas/config_v1_0.yaml @@ -0,0 +1,68 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +#$schema: "http://stsci.edu/schemas/yaml-schema/draft-01" +title: Exercise schema +type: object +required: + - title + - description + - instructions + - view_type + - files + - container +optional: + - radar_info + +definitions: + languageType: + type: string + enum: + - python + - scala + - javascript + +additionalProperties: false +properties: + title: + description: The name of the exercise + type: string + description: + type: string + instructions: + type: string + view_type: + type: string + files: + type: array + items: + type: object + required: + - field + properties: + field: + type: string + name: + type: string + container: + type: object + properties: + image: + type: string + mount: + type: string + cmd: + type: string + radar_info: + type: object + required: + - tokenizer + properties: + tokenizer: + $ref: '#/definitions/languageType' + #enum: + # - python + # - scala + # - javascript + minimum_match_tokens: + type: number + minimum: 0 diff --git a/apluslms_roman/schemas/config_v1_1.yaml b/apluslms_roman/schemas/config_v1_1.yaml new file mode 100644 index 0000000..7443a27 --- /dev/null +++ b/apluslms_roman/schemas/config_v1_1.yaml @@ -0,0 +1,68 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +#$schema: "http://stsci.edu/schemas/yaml-schema/draft-01" +title: Exercise schema +type: object +required: + - title + - description + - instructions + - view_type + - files + - container +optional: + - radar_info + +definitions: + languageType: + type: string + enum: + - python + - scala + - javascript + +additionalProperties: false +properties: + title: + description: The name of the exercise + type: string + description: + type: string + instructions: + type: string + view_type: + type: string + files: + type: array + items: + type: object + required: + - field + properties: + field: + type: string + name: + type: string + container: + type: object + properties: + image: + type: string + mount: + type: string + cmd: + type: string + radar_info: + type: object + required: + - tokenizer + properties: + tokenizer: + $ref: '#/definitions/languageType' + #enum: + # - python + # - scala + # - javascript + minimum_match_tokens: + type: number + minimum: 0 diff --git a/apluslms_roman/schemas/index_v1_0.yaml b/apluslms_roman/schemas/index_v1_0.yaml new file mode 100644 index 0000000..6d8e7bb --- /dev/null +++ b/apluslms_roman/schemas/index_v1_0.yaml @@ -0,0 +1,175 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +title: Index schema +type: object +definitions: + dateTime: + type: string + pattern: ^\d{4}\-\d{2}\-\d{2}(\s\d{2}\:\d{2})?$ + configFormat: + type: string + pattern: \.(json|yaml|yml)$ + statusEnum: + type: string + enum: + - ready + - unlisted + - enrollment + - enrollment_ext + - hidden + - maintenance + - nototal + + +required: + - categories + - end + - language + - modules + - name + - start + - static_dir + +additionalProperties: false +properties: + categories: + type: object + description: Categories can be added freely + additionalProperties: + type: object + required: + - name + properties: + name: + type: string + status: + $ref: '#/definitions/statusEnum' + end: + $ref: '#/definitions/dateTime' + language: + type: string + enum: + - en + - fi + modules: + type: array + additionalItems: false + items: + type: object + required: + - children + - key + # muita pakollisia? + properties: + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + allow_assistant_grading: + type: boolean + category: + type: string + config: + $ref: '#/definitions/configFormat' + confirm_the_level: + type: boolean + difficulty: + type: string + key: + type: string + lti: + type: string + lti_context_id: + type: string + lti_resource_link_id: + type: string + max_group_size: + type: number + minimum: 0 + max_points: + type: number + minimum: 0 + max_submissions: + type: number + minimum: 0 + min_group_size: + type: number + minimum: 0 + points_to_pass: + type: number + minimum: 0 + scale_points: + type: number + status: + $ref: '#/definitions/statusEnum' + title: + type: string + key: + type: string + name: + type: string + static_content: + type: string + use_wide_column: + type: boolean + + key: + type: string + name: + type: string + static_content: + type: string + status: + $ref: '#/definitions/statusEnum' + use_wide_column: + type: boolean + close: + $ref: '#/definitions/dateTime' + key: + type: string + late_close: + $ref: '#/definitions/dateTime' + late_penalty: + type: number + minimum: 0.0 + name: + type: string + open: + $ref: '#/definitions/dateTime' + status: + $ref: '#/definitions/statusEnum' + + name: + type: string + start: + $ref: '#/definitions/dateTime' + static_dir: + type: string diff --git a/apluslms_roman/schemas/index_v1_1.yaml b/apluslms_roman/schemas/index_v1_1.yaml new file mode 100644 index 0000000..6d8e7bb --- /dev/null +++ b/apluslms_roman/schemas/index_v1_1.yaml @@ -0,0 +1,175 @@ +--- +$schema: "http://json-schema.org/draft-04/schema" +title: Index schema +type: object +definitions: + dateTime: + type: string + pattern: ^\d{4}\-\d{2}\-\d{2}(\s\d{2}\:\d{2})?$ + configFormat: + type: string + pattern: \.(json|yaml|yml)$ + statusEnum: + type: string + enum: + - ready + - unlisted + - enrollment + - enrollment_ext + - hidden + - maintenance + - nototal + + +required: + - categories + - end + - language + - modules + - name + - start + - static_dir + +additionalProperties: false +properties: + categories: + type: object + description: Categories can be added freely + additionalProperties: + type: object + required: + - name + properties: + name: + type: string + status: + $ref: '#/definitions/statusEnum' + end: + $ref: '#/definitions/dateTime' + language: + type: string + enum: + - en + - fi + modules: + type: array + additionalItems: false + items: + type: object + required: + - children + - key + # muita pakollisia? + properties: + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + properties: + category: + type: string + children: + type: array + additionalItems: false + items: + type: object + required: + - category + - key + #additionalProperties: false + properties: + allow_assistant_grading: + type: boolean + category: + type: string + config: + $ref: '#/definitions/configFormat' + confirm_the_level: + type: boolean + difficulty: + type: string + key: + type: string + lti: + type: string + lti_context_id: + type: string + lti_resource_link_id: + type: string + max_group_size: + type: number + minimum: 0 + max_points: + type: number + minimum: 0 + max_submissions: + type: number + minimum: 0 + min_group_size: + type: number + minimum: 0 + points_to_pass: + type: number + minimum: 0 + scale_points: + type: number + status: + $ref: '#/definitions/statusEnum' + title: + type: string + key: + type: string + name: + type: string + static_content: + type: string + use_wide_column: + type: boolean + + key: + type: string + name: + type: string + static_content: + type: string + status: + $ref: '#/definitions/statusEnum' + use_wide_column: + type: boolean + close: + $ref: '#/definitions/dateTime' + key: + type: string + late_close: + $ref: '#/definitions/dateTime' + late_penalty: + type: number + minimum: 0.0 + name: + type: string + open: + $ref: '#/definitions/dateTime' + status: + $ref: '#/definitions/statusEnum' + + name: + type: string + start: + $ref: '#/definitions/dateTime' + static_dir: + type: string diff --git a/apluslms_roman/validator.py b/apluslms_roman/validator.py new file mode 100644 index 0000000..6bf0521 --- /dev/null +++ b/apluslms_roman/validator.py @@ -0,0 +1,97 @@ +import yaml +from jsonschema import validate, ValidationError +import re +import os +#from operator import itemgetter +import logging + +logger = logging.getLogger(__name__) + +#List of (possible) custom schema locations will be in config file +def get_folders(config): + folders = config.__config__.get("schema_path", []) + if not isinstance(folders, list): + raise ValueError("Invalid type for 'schema_path' in config. Excpected a list.") + dir_, _ = os.path.split(__file__) + folders.append(os.path.join(dir_, "schemas")) + return folders + +#Format of index: { schema_name: {(major, minor): full_filepath}} +SCHEMA_INDEX = {} + +class Validator: + def __init__(self, config): + logger.info(" Creating validator.") + self.folders = get_folders(config) + logger.info(" Validator ready.") + + def validate(self, filename, schema, major=1, minor=None): + logger.info(" Preparing to validate %s...", filename) + logger.info(" Locating all valid schemas...") + self.find_all_matches(schema) + logger.info(" Locating matching version v%d...", major) + serial, schema_path = self.find_newest(schema, major, minor) + #schema_fullname = "{}_v{}_{}".format(schema, serial[0], serial[1]) + #schema_path = os.path.join(ext[0], schema_fullname) + logger.info(" Starting validation of %s with %s", filename, schema_path) + return self.assert_valid(schema_path, filename) + + + def find_all_matches(self, schema): + prog = re.compile("{}_v(\d+)_(\d+).(yaml|yml|json)".format(schema)) + logger.debug(" Looking for schema-------------%s", schema) + for folder in self.folders: + logger.debug(" Folder-------------%s", folder) + for ff in os.listdir(folder): + logger.debug(" File-------------%s", ff) + if prog.match(ff) is not None: + major, minor, ext = prog.match(ff).groups() + schema_i = SCHEMA_INDEX.setdefault(schema, {}) + logger.debug(" Index-------------%s", SCHEMA_INDEX) + current = schema_i.setdefault((int(major), int(minor)), None) + if current is None: + schema_i[int(major), int(minor)] = os.path.join(folder, ff) + + #Finds the newest schema with given 'major' and newest 'minor'. + #If major is not defined, v1 is assumed. + def find_newest(self, schema, major, minor): + schemas = SCHEMA_INDEX.get(schema) + if schemas is None: + logger.warning(" No schema's with that name!") + return None + + if minor is None: + ver = max({v for v in schemas if v[0]==major}, default=None) + if ver is None: + logger.warning(" No schema with version %d", major) + return None + else: + ver = (major, minor) + + _file = schemas.get(ver, None) + if _file is None: + logger.warning(" No schema with version %d.%d", major, minor) + return None + return ver, _file + + + #Validate schema + def assert_valid(self, schema, filename): + try: + with open(filename, "r") as f: + ff = f.read() + with open(schema, "r") as s: + ss = s.read() + except: + logger.exception(' Error with files, exiting....') + return False + + try: + logger.info(' Files opened succesfully. Validating...') + validate(yaml.safe_load(ff), yaml.safe_load(ss)) + logger.info(' %s validated succesfully', filename) + return True + except ValidationError as e: + logger.warning(" An error occurred!") + logger.warning(" %s", e) + return False diff --git a/requirements.txt b/requirements.txt index c32bca5..11748dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ PyYaml>=3.12 Docker>=3.0.0 +jsonschema>=2.6.0 diff --git a/requirements_build_linux.txt b/requirements_build_linux.txt index 64faff7..4241414 100644 --- a/requirements_build_linux.txt +++ b/requirements_build_linux.txt @@ -1 +1,2 @@ PyInstaller >= 3.2.1 +tk diff --git a/simple_gui/setup.py b/simple_gui/setup.py index 8070692..b82fce9 100755 --- a/simple_gui/setup.py +++ b/simple_gui/setup.py @@ -49,7 +49,7 @@ ], zip_safe=True, - #packages=find_packages(exclude=['contrib', 'docs', 'tests']), + packages=find_packages(exclude=['contrib', 'docs', 'tests']), py_modules=['roman_tki'], include_package_data = True, package_data={