From 53352de6869c6bdff6a36706d85a9f35de01184f Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 10 Feb 2019 19:24:30 +0100 Subject: [PATCH 1/9] Add the Monday course in Brno --- runs/2019/brno-jaro-2019-pondeli/link.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 runs/2019/brno-jaro-2019-pondeli/link.yml diff --git a/runs/2019/brno-jaro-2019-pondeli/link.yml b/runs/2019/brno-jaro-2019-pondeli/link.yml new file mode 100644 index 0000000000..ee90e46ed8 --- /dev/null +++ b/runs/2019/brno-jaro-2019-pondeli/link.yml @@ -0,0 +1,2 @@ +repo: https://github.com/encukou/naucse.python.cz/ +branch: beginners-2019-exp From 3cfc0e39244875708cafceab1bbb601c817183f8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 18 Feb 2019 13:26:00 +0100 Subject: [PATCH 2/9] Add the Wednesday course in Brno --- runs/2019/pyladies-brno-jaro-st/link.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 runs/2019/pyladies-brno-jaro-st/link.yml diff --git a/runs/2019/pyladies-brno-jaro-st/link.yml b/runs/2019/pyladies-brno-jaro-st/link.yml new file mode 100644 index 0000000000..79143c49e8 --- /dev/null +++ b/runs/2019/pyladies-brno-jaro-st/link.yml @@ -0,0 +1,2 @@ +repo: https://github.com/woodmaker/naucse.python.cz +branch: master From 86f8ab8abb9aba3ef384d50697eff141b6d03630 Mon Sep 17 00:00:00 2001 From: Lumir Balhar Date: Wed, 20 Feb 2019 10:25:13 +0100 Subject: [PATCH 3/9] Ostrava: Add link to Git workshop materials --- runs/2019/ostrava-git-workshop/link.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 runs/2019/ostrava-git-workshop/link.yml diff --git a/runs/2019/ostrava-git-workshop/link.yml b/runs/2019/ostrava-git-workshop/link.yml new file mode 100644 index 0000000000..b8c5fd34c9 --- /dev/null +++ b/runs/2019/ostrava-git-workshop/link.yml @@ -0,0 +1,2 @@ +repo: https://github.com/frenzymadness/naucse.python.cz.git +branch: ostrava_git_workshop \ No newline at end of file From 92e84a4633b6e1a0b07d270534d1437dcb0cf4be Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sat, 23 Feb 2019 21:45:55 +0100 Subject: [PATCH 4/9] converters: Pass contexts to all load/dump operations The API version determines how data is loaded/dumped, so it needs to be available or all the load/dump methods. Currently the naucse API version is two numbers, expected to increase linearly, but I could imagine e.g. branching versioning in the future (for example with "preview feature" tags, like what GitHub does in its API). That's why I don't use "version" directly, but wrap it in an abstract "context". The top-level dump() function now requires a version, and creates a context. The load() methods should not be called top-level, but always via the top-level load() function (which reads the API version from passed-in data, and creates a context). Adjust models and tests. --- naucse/converters.py | 101 ++++++++++++++++++++---------- naucse/models.py | 64 ++++++++++--------- test_naucse/conftest.py | 4 ++ test_naucse/test_arca_render.py | 6 +- test_naucse/test_course.py | 6 +- test_naucse/test_model.py | 12 ++-- test_naucse/test_session_times.py | 8 +-- 7 files changed, 122 insertions(+), 79 deletions(-) diff --git a/naucse/converters.py b/naucse/converters.py index 4b7b18ea46..e1c1244668 100644 --- a/naucse/converters.py +++ b/naucse/converters.py @@ -80,9 +80,12 @@ class BaseConverter: def _naucse__converter(self): return self - def load(self, data, **kwargs): + def load(self, data, context, **kwargs): """Convert a JSON-compatible data to a Python value. + `context` is a `LoadContext`, which holds options (like the API + version) for loading an entire tree of objects. + `kwargs` are extra keyword arguments passed to `__init__`. The Converter's `load_arg_names` attribute specifies which kwargs are supported. @@ -91,9 +94,12 @@ def load(self, data, **kwargs): """ return data - def dump(self, value): + def dump(self, value, context): """Convert a Python value to JSON-compatible data. + `context` is a `DumpContext`, which holds options (like the API + version) for dumping an entire tree of objects. + The base implementation returns `value` unchanged. """ return value @@ -125,7 +131,7 @@ def get_schema(self, context): class IntegerConverter(BaseConverter): - def load(self, data): + def load(self, data, context): return int(data) def get_schema(self, context): @@ -133,7 +139,7 @@ def get_schema(self, context): class FloatConverter(BaseConverter): - def load(self, data): + def load(self, data, context): return float(data) def get_schema(self, context): @@ -173,16 +179,16 @@ def __init__(self, item_converter, *, index_arg=None): self.load_arg_names = self.item_converter.load_arg_names self.index_arg = index_arg - def load(self, data, **kwargs): + def load(self, data, context, **kwargs): result = [] for index, d in enumerate(data): if self.index_arg: kwargs[self.index_arg] = index - result.append(self.item_converter.load(d, **kwargs)) + result.append(self.item_converter.load(d, context, **kwargs)) return result - def dump(self, value): - return [self.item_converter.dump(v) for v in value] + def dump(self, value, context): + return [self.item_converter.dump(v, context) for v in value] def get_schema(self, context): return { @@ -207,16 +213,19 @@ def __init__(self, item_converter, *, key_arg=None, required=()): self.key_arg = key_arg self.required = required - def load(self, data, **kwargs): + def load(self, data, context, **kwargs): result = {} for k, v in data.items(): if self.key_arg: kwargs[self.key_arg] = k - result[k] = self.item_converter.load(v, **kwargs) + result[k] = self.item_converter.load(v, context, **kwargs) return result - def dump(self, value): - return {str(k): self.item_converter.dump(v) for k, v in value.items()} + def dump(self, value, context): + return { + str(k): self.item_converter.dump(v, context) + for k, v in value.items() + } def get_schema(self, context): schema = { @@ -243,17 +252,17 @@ def __init__(self, item_converter, *, key_attr, index_arg=None): self.index_arg = index_arg self.load_arg_names = set(self.item_converter.load_arg_names) - def load(self, data, **kwargs): + def load(self, data, context, **kwargs): result = {} for index, value in enumerate(data): if self.index_arg: kwargs[self.index_arg] = index - item = self.item_converter.load(value, **kwargs) + item = self.item_converter.load(value, context, **kwargs) result[getattr(item, self.key_attr)] = item return result - def dump(self, value): - return [self.item_converter.dump(v) for k, v in value.items()] + def dump(self, value, context): + return [self.item_converter.dump(v, context) for k, v in value.items()] def get_schema(self, context): return { @@ -327,7 +336,7 @@ def __set_name__(self, cls, name): self.name = name self.data_key = self.data_key or self.name - def load_into(self, instance, data, **kwargs): + def load_into(self, instance, data, context, **kwargs): """Load this field's data into the given Python object. `instance` is the Python object being initialized. @@ -357,10 +366,10 @@ def load_into(self, instance, data, **kwargs): n: v for n, v in kwargs.items() if n in self.converter.load_arg_names } - value = self.converter.load(item_data, **kwargs) + value = self.converter.load(item_data, context, **kwargs) setattr(instance, self.name, value) for func in self._after_load_hooks: - func(instance) + func(instance, context) def _get_default(self, instance): """Return the default value (for optional fields). @@ -369,7 +378,7 @@ def _get_default(self, instance): """ return None - def dump_into(self, instance, data): + def dump_into(self, instance, data, context): """Dump the given Python object into the given JSON-compatible dict If the field is not marked `output`, or is optional and has the default @@ -380,7 +389,7 @@ def dump_into(self, instance, data): value = getattr(instance, self.name) if self.optional and value == self.default: return - data[self.data_key] = self.converter.dump(value) + data[self.data_key] = self.converter.dump(value, context) def put_schema_into(self, object_schema, context): if context.is_input and not self.input: @@ -462,16 +471,16 @@ def __init__( def __repr__(self): return f'<{_classname(type(self))} for {_classname(self.cls)}>' - def load(self, data, **kwargs): + def load(self, data, context, **kwargs): result = self.cls(**kwargs) for field in self.fields.values(): - field.load_into(result, data, parent=result) + field.load_into(result, data, context, parent=result) return result - def dump(self, value): + def dump(self, value, context): result = {} for field in self.fields.values(): - field.dump_into(value, result) + field.dump_into(value, result, context) return result def get_schema(self, context): @@ -487,16 +496,37 @@ def get_schema(self, context): return schema +class LoadContext: + """Holds "global" options for loading data + + `version` is the API version, as a tuple of ints (major, minor). + """ + def __init__(self, version): + self.version = version + + +class DumpContext: + """Holds "global" options for dumping data + + `version` is the API version, as a tuple of ints (major, minor). + """ + def __init__(self, version): + self.version = version + + class SchemaContext: """Holds "global" definitions and options for getting a context `is_input` determines whether schema for input (data from forks) or output (naucse's exported API). + + `version` is the API version, as a tuple of ints (major, minor). """ - def __init__(self, *, is_input): + def __init__(self, *, is_input, version): self.definition_refs = {} self.definitions = {} self.is_input = is_input + self.version = version def get_schema(self, converter): """Get schema for the given converter @@ -516,10 +546,10 @@ def get_schema(self, converter): return converter.get_schema(self) -def get_schema(converter, *, is_input): +def get_schema(converter, *, is_input, version): """Get schema for the given converter""" converter = get_converter(converter) - context = SchemaContext(is_input=is_input) + context = SchemaContext(is_input=is_input, version=version) context.definitions.update({ 'ref': { 'type': 'object', @@ -575,7 +605,7 @@ def _get_schema_url(converter, instance): raise ValueError(f"{converter}.get_schema_url is None") -def dump(instance, converter=None): +def dump(instance, converter=None, *, version): """Dump a Python object If converter is None, the default is used. @@ -584,23 +614,26 @@ def dump(instance, converter=None): converter = get_converter(instance) converter = get_converter(converter) slug = converter.slug or 'data' + context = DumpContext(version=version) result = { - 'api_version': [0, 0], - slug: converter.dump(instance), + 'api_version': context.version, + slug: converter.dump(instance, context), } result['$schema'] = _get_schema_url(converter, instance) - schema = get_schema(converter, is_input=False) + schema = get_schema(converter, is_input=False, version=context.version) jsonschema.validate(result, schema) return result def load(converter, data, **kwargs): """Load a Python object from the given data""" + version = data['api_version'] converter = get_converter(converter) - schema = get_schema(converter, is_input=True) + context = LoadContext(version=version) + schema = get_schema(converter, is_input=True, version=context.version) jsonschema.validate(data, schema) slug = converter.slug or 'data' - return converter.load(data[slug], **kwargs) + return converter.load(data[slug], context, **kwargs) def register_model(cls, converter=None): diff --git a/naucse/models.py b/naucse/models.py index 6cbb01699a..292a3c86ec 100644 --- a/naucse/models.py +++ b/naucse/models.py @@ -32,10 +32,10 @@ class NoURLType(NoURL): class URLConverter(BaseConverter): - def load(self, data): + def load(self, data, context): return sanitize.convert_link('href', data) - def dump(self, value): + def dump(self, value, context): return value @classmethod @@ -156,12 +156,12 @@ class HTMLFragmentConverter(BaseConverter): def __init__(self, *, sanitizer=None): self.sanitizer = sanitizer - def load(self, value, parent): + def load(self, value, context, *, parent): if self.sanitizer is None: return sanitize.sanitize_html(value) return self.sanitizer(parent, value) - def dump(self, value): + def dump(self, value, context): return str(value) @classmethod @@ -187,10 +187,10 @@ class Solution(Model): class RelativePathConverter(BaseConverter): """Converter for a relative path, as string""" - def load(self, data): + def load(self, data, context): return Path(data) - def dump(self, value): + def dump(self, value, context): return str(value) def get_schema(self, context): @@ -208,7 +208,7 @@ def get_schema(self, context): + "relative to the repository root") @source_file_field.after_load() -def _edit_info(self): +def _edit_info(self, context): if self.source_file is None: self.edit_info = None else: @@ -234,10 +234,10 @@ def get_pks(self): class PageCSSConverter(BaseConverter): """Converter for CSS for a Page""" - def load(self, value): + def load(self, value, context): return sanitize.sanitize_css(value) - def dump(self, value): + def dump(self, value, context): return value @classmethod @@ -252,10 +252,10 @@ class LicenseConverter(BaseConverter): """Converter for a licence (specified as its slug in JSON)""" load_arg_names = {'parent'} - def load(self, value, parent): + def load(self, value, context, *, parent): return parent.root.licenses[value] - def dump(self, value): + def dump(self, value, context): return value.slug @classmethod @@ -351,7 +351,7 @@ class Material(Model): doc="Slug of the corresponding lesson") @lesson_slug.after_load() - def _validate_lesson_slug(self): + def _validate_lesson_slug(self, context): if self.lesson_slug and self.external_url: raise ValueError( 'external_url and lesson_slug are incompatible' @@ -416,7 +416,7 @@ class SessionTimeConverter(BaseConverter): to be fixed up using `_combine_session_time`. Converted to the full datetime on output. """ - def load(self, data): + def load(self, data, context): try: return datetime.datetime.strptime('%Y-%m-%d %H:%M:%S', data) except ValueError: @@ -426,7 +426,7 @@ def load(self, data): time = datetime.datetime.strptime(data, '%H:%M').time() return time.replace(tzinfo=dateutil.tz.gettz(_TIMEZONE)) - def dump(self, value): + def dump(self, value, context): return value.strftime('%Y-%m-%d %H:%M:%S') @classmethod @@ -446,10 +446,10 @@ def get_schema(cls, context): class DateConverter(BaseConverter): """Converter for datetime.date values (as 'YYYY-MM-DD' strings in JSON)""" - def load(self, data): + def load(self, data, context): return datetime.datetime.strptime(data, "%Y-%m-%d").date() - def dump(self, value): + def dump(self, value, context): return str(value) def get_schema(self, context): @@ -490,7 +490,7 @@ class Session(Model): ) @materials.after_load() - def _index_materials(self): + def _index_materials(self, context): set_prev_next(m for m in self.materials if m.lesson_slug) pages = Field( @@ -498,13 +498,15 @@ def _index_materials(self): optional=True, doc="The session's cover pages") @pages.after_load() - def _set_pages(self): + def _set_pages(self, context): if not self.pages: self.pages = {} for slug in 'front', 'back': if slug not in self.pages: - page = get_converter(SessionPage).load( - {}, slug=slug, parent=self, + page = load( + SessionPage, + {'api_version': [0, 0], 'session-page': {}}, + slug=slug, parent=self, ) self.pages[slug] = page @@ -514,7 +516,7 @@ def _set_pages(self): doc="Time when this session takes place.") @time.after_load() - def _fix_time(self): + def _fix_time(self, context): if self.time is None: self.time = {} else: @@ -546,10 +548,10 @@ def _fix_time(self): class AnyDictConverter(BaseConverter): """Converter of any JSON-encodable dict""" - def load(self, data): + def load(self, data, context): return data - def dump(self, value): + def dump(self, value, context): return value @classmethod @@ -568,13 +570,13 @@ def time_from_string(time_string): class TimeIntervalConverter(BaseConverter): """Converter for a time interval, as a dict with 'start' and 'end'""" - def load(self, data): + def load(self, data, context): return { 'start': time_from_string(data['start']), 'end': time_from_string(data['end']), } - def dump(self, value): + def dump(self, value, context): return { 'start': value['start'].strftime('%H:%M'), 'end': value['end'].strftime('%H:%M'), @@ -673,7 +675,7 @@ def _default_lessons(self): doc="Individual sessions") @sessions.after_load() - def _sessions_after_load(self): + def _sessions_after_load(self, context): set_prev_next(self.sessions.values()) for session in self.sessions.values(): @@ -734,7 +736,7 @@ def load_remote(cls, slug, *, parent, link_info): doc="Slug of the course this derives from (deprecated)") @derives.after_load() - def _set_base_course(self): + def _set_base_course(self, context): key = f'courses/{self.derives}' try: self.base_course = self.root.courses[key] @@ -819,7 +821,7 @@ def freeze(self): class AbbreviatedDictConverter(DictConverter): """Dict that only shows URLs to its items when dumped""" - def dump(self, value): + def dump(self, value, context): return { key: {'$ref': v.get_url('api', external=True)} for key, v in value.items() @@ -1034,7 +1036,11 @@ def load_licenses(self, path): with (licence_path / 'info.yml').open() as f: info = yaml.safe_load(f) slug = licence_path.name - license = get_converter(License).load(info, parent=self, slug=slug) + license = load( + License, + {'api_version': [0, 0], 'license': info}, + parent=self, slug=slug, + ) self.licenses[slug] = license def get_course(self, slug): diff --git a/test_naucse/conftest.py b/test_naucse/conftest.py index b377affa17..af963d9e42 100644 --- a/test_naucse/conftest.py +++ b/test_naucse/conftest.py @@ -82,6 +82,10 @@ def assert_yaml_dump(data, filename): assert data == expected +def assert_model_dump(model, filename): + assert_yaml_dump(models.dump(model, version=[0, 0]), filename) + + def add_test_course(model, slug, data): model.add_course(models.load( models.Course, diff --git a/test_naucse/test_arca_render.py b/test_naucse/test_arca_render.py index fef5019451..2a97e80584 100644 --- a/test_naucse/test_arca_render.py +++ b/test_naucse/test_arca_render.py @@ -13,7 +13,7 @@ from naucse.arca_renderer import RemoteRepoError from test_naucse.conftest import fixture_path, make_model, get_local_repo_info -from test_naucse.conftest import assert_yaml_dump +from test_naucse.conftest import assert_model_dump def run(args, *, cwd, check=True, env=None, **kwargs): @@ -81,7 +81,7 @@ def test_valid_fork(arca_model, content_repo): link_info={'repo': content_repo.as_uri()}, ) arca_model.add_course(course) - assert_yaml_dump(models.dump(course), 'normal-course.yaml') + assert_model_dump(course, 'normal-course.yaml') def test_yaml_error(arca_model, content_repo, git_command): @@ -212,7 +212,7 @@ def test_fork_link(arca_model, content_repo, tmp_path, slug): ) arca_model.load_local_courses(tmp_path / 'data') course = arca_model.courses[slug] - assert_yaml_dump(models.dump(course), LINK_INFO[slug]['expected_file']) + assert_model_dump(course, LINK_INFO[slug]['expected_file']) def test_bad_fork_link(arca_model, content_repo, tmp_path): diff --git a/test_naucse/test_course.py b/test_naucse/test_course.py index a2a1de4fea..9cbd1c5cbf 100644 --- a/test_naucse/test_course.py +++ b/test_naucse/test_course.py @@ -7,7 +7,7 @@ from naucse import models from naucse.edit_info import get_local_repo_info -from test_naucse.conftest import assert_yaml_dump, add_test_course +from test_naucse.conftest import assert_model_dump, add_test_course from test_naucse.conftest import fixture_path class DummyRenderer: @@ -118,7 +118,7 @@ def test_empty_course_from_renderer(model): renderer=renderer, ) check_empty_course_attrs(course, source_file=Path(source)) - assert_yaml_dump(models.dump(course), 'minimal-course.yml') + assert_model_dump(course, 'minimal-course.yml') def load_course_from_fixture(model, filename): @@ -143,7 +143,7 @@ def test_complex_course(model): """Valid complex json that could come from a fork is loaded correctly""" course = load_course_from_fixture(model, 'course-data/complex-course.yml') - assert_yaml_dump(models.dump(course), 'complex-course.yml') + assert_model_dump(course, 'complex-course.yml') # Make sure HTML is sanitized assert course.long_description == 'A fun course!' diff --git a/test_naucse/test_model.py b/test_naucse/test_model.py index 3c6d6d288a..fee4a1bf70 100644 --- a/test_naucse/test_model.py +++ b/test_naucse/test_model.py @@ -8,7 +8,7 @@ from naucse.edit_info import get_local_repo_info from test_naucse.conftest import fixture_path, dummy_schema_url_factory -from test_naucse.conftest import assert_yaml_dump, add_test_course +from test_naucse.conftest import assert_model_dump, add_test_course def test_empty_model(): @@ -38,7 +38,7 @@ def test_licenses(): def test_dump_empty_model(): model = models.Root(schema_url_factory=dummy_schema_url_factory) - assert_yaml_dump(models.dump(model), 'empty-root.yml') + assert_model_dump(model, 'empty-root.yml') def test_load_empty_dir(): @@ -101,9 +101,9 @@ def test_dump_local_course(model): slug='courses/minimal', )) - assert_yaml_dump(models.dump(model), 'minimal-root.yml') + assert_model_dump(model, 'minimal-root.yml') course = model.courses['courses/minimal'] - assert_yaml_dump(models.dump(course), 'minimal-course.yml') + assert_model_dump(course, 'minimal-course.yml') def test_add_course_from_data(): @@ -195,6 +195,6 @@ def test_run_years(model): '2019/single-session': course_2019, } - assert_yaml_dump(models.dump(model), 'run-years/root.yml') + assert_model_dump(model, 'run-years/root.yml') for year, run_year in model.run_years.items(): - assert_yaml_dump(models.dump(run_year), f'run-years/{year}.yml') + assert_model_dump(run_year, f'run-years/{year}.yml') diff --git a/test_naucse/test_session_times.py b/test_naucse/test_session_times.py index 0b846d771a..2f756192cf 100644 --- a/test_naucse/test_session_times.py +++ b/test_naucse/test_session_times.py @@ -6,7 +6,7 @@ from naucse import models from test_naucse.conftest import fixture_path, add_test_course -from test_naucse.conftest import assert_yaml_dump +from test_naucse.conftest import assert_model_dump TZINFO = dateutil.tz.gettz('Europe/Prague') @@ -71,7 +71,7 @@ def test_run_with_default_times(model): assert session.date == None assert session.time == None - assert_yaml_dump(models.dump(course), 'session-times/with-default-times') + assert_model_dump(course, 'session-times/with-default-times') def test_course_with_no_default_time(model): @@ -102,7 +102,7 @@ def test_course_with_no_default_time(model): assert session.date == None assert session.time == None - assert_yaml_dump(models.dump(course), 'session-times/without-default-time') + assert_model_dump(course, 'session-times/without-default-time') def test_course_without_dates(model): @@ -123,7 +123,7 @@ def test_course_without_dates(model): assert session.date is None assert session.time is None - assert_yaml_dump(models.dump(course), 'session-times/without-dates') + assert_model_dump(course, 'session-times/without-dates') BAD_TIMES = { From 384520329653fda8b5500a827f484e41673118e9 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sat, 23 Feb 2019 23:17:27 +0100 Subject: [PATCH 5/9] converters: Make it possible to have version-specific fields in the API --- naucse/converters.py | 136 +++++++++++++++++++----- test_naucse/test_converters_versions.py | 121 +++++++++++++++++++++ 2 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 test_naucse/test_converters_versions.py diff --git a/naucse/converters.py b/naucse/converters.py index e1c1244668..23a19a8bc9 100644 --- a/naucse/converters.py +++ b/naucse/converters.py @@ -275,7 +275,33 @@ def _classname(cls): return f'{cls.__module__}.{cls.__qualname__}' -class Field: +class AbstractField: + """Descriptor for a Model's attribute that is loaded/dumped to JSON + + See Field for the API. + """ + + def __get__(self, instance, owner): + """Debug helper + + An initialized model instance should have values for all its fields + in its __dict__. + + However, when loading, not all fields have been initialized yet. + Attempts to get the value of such a field will raise an informative + error, rather than return the Field object from the model class. + """ + if instance is None: + # Getting a class attribute -- return this Field + return self + else: + # Getting an instnce attribute -- raise an error + type_name = owner.__name__ + raise AttributeError( + f'{self.name!r} of {type_name} object was not yet loaded' + ) + +class Field(AbstractField): """Descriptor for a Model's attribute that is loaded/dumped to JSON `converter`: Converter to use for the attribute. @@ -406,26 +432,6 @@ def put_schema_into(self, object_schema, context): if not context.is_input: object_schema['additionalProperties'] = False - def __get__(self, instance, owner): - """Debug helper - - An initialized model instance should have values for all its fields - in its __dict__. - - However, when loading, not all fields have been initialized yet. - Attempts to get the value of such a field will raise an informative - error, rather than return the Field object from the model class. - """ - if instance is None: - # Getting a class attribute -- return this Field - return self - else: - # Getting an instnce attribute -- raise an error - type_name = owner.__name__ - raise AttributeError( - f'{self.name!r} of {type_name} object was not yet loaded' - ) - def default_factory(self): """Decorate a function that will be called to produce a default value @@ -450,6 +456,78 @@ def _decorator(func): return _decorator +class VersionField(AbstractField): + """Chooses Field based on the API version + + `fields` should be a {version introduced: field} mapping. + When loading/dumping/getting schema, VersionField picks the field for + tat version and forwards the operation to it. + For versions before the first specified, the field is not loaded/dumped, + and the instance attribute is set to None. + + VersionField adds a "Added/Modified in API version" note to the JSON Schema + description. + + Making later fields suitably backwards-compatible is the user's + responsibility. + """ + + def __init__(self, fields, name=None): + self.fields = sorted((tuple(k), f) for k, f in fields.items()) + self.name = name + + def _field_for_context(self, context): + for version, field in reversed(self.fields): + if version <= context.version: + return version, field + return None, None + + def __repr__(self): + return f'<{_classname(type(self))} {self.name} ({self.fields})>' + + def __set_name__(self, cls, name): + self.name = name + for version, field in self.fields: + set_name = getattr(type(field), '__set_name__', None) + if set_name: + set_name(field, cls, name) + + def load_into(self, instance, data, context, **kwargs): + version, field = self._field_for_context(context) + if field: + field.load_into(instance, data, context, **kwargs) + else: + setattr(instance, self.name, None) + + def dump_into(self, instance, data, context): + version, field = self._field_for_context(context) + if field: + field.dump_into(instance, data, context) + + def put_schema_into(self, object_schema, context): + version, field = self._field_for_context(context) + if field: + field.put_schema_into(object_schema, context) + try: + schema = object_schema['properties'][self.name] + except KeyError: + pass + if version == self.fields[0][0]: + note = 'Added in API version {}.{}'.format(*version) + else: + note = 'Modified in API version {}.{}'.format(*version) + if 'description' in schema: + schema['description'] += '\n\n' + note + else: + schema['description'] = note + + def default_factory(self): + raise NotImplementedError('default_factory is not implemented yet') + + def after_load(self): + raise NotImplementedError('after_load is not implemented yet') + + class ModelConverter(BaseConverter): """Converter for a Model, i.e. class with several Fields""" def __init__( @@ -457,13 +535,17 @@ def __init__( ): self.cls = cls self.name = cls.__name__ - self.doc = inspect.getdoc(cls).strip() + doc = inspect.getdoc(cls) + if doc: + self.doc = doc.strip() + else: + self.doc = '' self.fields = {} self.load_arg_names = load_arg_names self.slug = slug for name, field in vars(cls).items(): - if name.startswith('__') or not isinstance(field, Field): + if name.startswith('__') or not isinstance(field, AbstractField): continue self.fields[name] = field self.fields.update((f.name, f) for f in extra_fields) @@ -502,7 +584,7 @@ class LoadContext: `version` is the API version, as a tuple of ints (major, minor). """ def __init__(self, version): - self.version = version + self.version = tuple(version) class DumpContext: @@ -511,7 +593,7 @@ class DumpContext: `version` is the API version, as a tuple of ints (major, minor). """ def __init__(self, version): - self.version = version + self.version = tuple(version) class SchemaContext: @@ -526,7 +608,7 @@ def __init__(self, *, is_input, version): self.definition_refs = {} self.definitions = {} self.is_input = is_input - self.version = version + self.version = tuple(version) def get_schema(self, converter): """Get schema for the given converter @@ -616,7 +698,7 @@ def dump(instance, converter=None, *, version): slug = converter.slug or 'data' context = DumpContext(version=version) result = { - 'api_version': context.version, + 'api_version': list(context.version), slug: converter.dump(instance, context), } result['$schema'] = _get_schema_url(converter, instance) diff --git a/test_naucse/test_converters_versions.py b/test_naucse/test_converters_versions.py new file mode 100644 index 0000000000..e6f8e34fb4 --- /dev/null +++ b/test_naucse/test_converters_versions.py @@ -0,0 +1,121 @@ +import pytest +from jsonschema.exceptions import ValidationError + +from naucse.converters import Field, VersionField, load, dump, get_schema +from naucse.converters import register_model, get_converter + + +class TestModel: + versioned_field = VersionField({ + # (Versions are out of order to test that VersionField sorts them) + (0, 1): Field(str, optional=True, doc='Introducing new field'), + (1, 0): Field(int, optional=True, doc="Let's make it an int"), + (0, 5): Field(bool, optional=True, doc="Actually it's a bool"), + (2, 0): Field(int, doc='No longer optional'), + }) + +register_model(TestModel) +get_converter(TestModel).get_schema_url = lambda *a, **ka: "" + +TEST_DATA = { + (0, 1): "a", + (0, 2): "b", + (0, 5): True, + (0, 6): False, + (1, 0): 123, + (2, 1): 456, +} + + +@pytest.mark.parametrize( + 'version', + ((0, 0), (0, 1), (0, 2), (0, 5), (0, 6), (1, 0)), +) +def test_load_nothing(version): + result = load(TestModel, { + 'api_version': list(version), + 'data': {}, + }) + assert result.versioned_field == None + + +def test_not_optional(): + with pytest.raises(ValidationError): + load(TestModel, { + 'api_version': [2, 0], + 'data': {}, + }) + + +@pytest.mark.parametrize(('version', 'data'), TEST_DATA.items()) +def test_load_data(version, data): + result = load(TestModel, { + 'api_version': list(version), + 'data': {'versioned_field': data}, + }) + assert result.versioned_field == data + + +@pytest.mark.parametrize( + ('version', 'data'), + ( + ((0, 0), 123), + ((0, 0), "ab"), + ((0, 1), True), + ((1, 0), "ab"), + ((1, 1), "ab"), + ), +) +def test_load_wrong_data(version, data): + with pytest.raises(ValidationError): + result = load(TestModel, { + 'api_version': (0, 0), + 'data': {'versioned_field': 123}, + }) + + +@pytest.mark.parametrize(('version', 'data'), TEST_DATA.items()) +def test_dump_data(version, data): + data_dict = { + 'api_version': list(version), + 'data': {'versioned_field': data}, + } + result = load(TestModel, data_dict) + assert dump(result, version=version) == {'$schema': '', **data_dict} + + +@pytest.mark.parametrize(('version', 'data'), TEST_DATA.items()) +def test_dump_v0(version, data): + data_dict = { + 'api_version': list(version), + 'data': {'versioned_field': data}, + } + result = load(TestModel, data_dict) + assert dump(result, version=(0, 0)) == { + '$schema': '', + 'api_version': [0, 0], + 'data': {}, + } + + +@pytest.mark.parametrize( + ('version', 'expected'), + { + (0, 0): None, + (0, 1): "Introducing new field\n\nAdded in API version 0.1", + (0, 2): "Introducing new field\n\nAdded in API version 0.1", + (0, 5): "Actually it's a bool\n\nModified in API version 0.5", + (1, 0): "Let's make it an int\n\nModified in API version 1.0", + (1, 1): "Let's make it an int\n\nModified in API version 1.0", + (2, 1): 'No longer optional\n\nModified in API version 2.0', + }.items() +) +def test_doc(version, expected): + schema = get_schema(TestModel, is_input=True, version=version) + print(schema) + properties = schema['properties']['data']['properties'] + if expected == None: + assert 'versioned_field' not in properties + else: + doc = properties['versioned_field']['description'] + assert doc == expected From 52a5b4e29f53d4f70e8adf7f82f082d915c97d77 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sat, 23 Feb 2019 23:17:52 +0100 Subject: [PATCH 6/9] models: Add Session.serial to the API --- naucse/models.py | 24 ++++++++++++++++++++++-- naucse/templates/course.html | 4 ++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/naucse/models.py b/naucse/models.py index 292a3c86ec..34900bbc4d 100644 --- a/naucse/models.py +++ b/naucse/models.py @@ -10,8 +10,8 @@ from arca import Task from naucse.edit_info import get_local_repo_info, get_repo_info -from naucse.converters import Field, register_model, BaseConverter -from naucse.converters import ListConverter, DictConverter +from naucse.converters import Field, VersionField, register_model +from naucse.converters import BaseConverter, ListConverter, DictConverter from naucse.converters import KeyAttrDictConverter, ModelConverter from naucse.converters import dump, load, get_converter, get_schema from naucse import sanitize @@ -476,6 +476,21 @@ class Session(Model): DateConverter(), optional=True, doc="The date when this session occurs (if it has a set time)", ) + serial = VersionField({ + (0, 1): Field( + str, + optional=True, + doc=""" + Human-readable string identifying the session's position + in the course. + The serial is usually numeric: `1`, `2`, `3`, ..., + but, for example, i, ii, iii... can be used for appendices. + Some courses start numbering sessions from 0. + """ + ), + # For API version 0.0, serial is generated in + # Course._sessions_after_load. + }) description = Field( HTMLFragmentConverter(), optional=True, @@ -683,6 +698,11 @@ def _sessions_after_load(self, context): if material.lesson_slug: self._requested_lessons.add(material.lesson_slug) + if context.version < (0, 1) and len(self.sessions) > 1: + # Assign serials to sessions (numbering from 1) + for serial, session in enumerate(self.sessions.values(), start=1): + session.serial = str(serial) + source_file = source_file_field start_date = Field( diff --git a/naucse/templates/course.html b/naucse/templates/course.html index c4a416cfba..6cac65b015 100644 --- a/naucse/templates/course.html +++ b/naucse/templates/course.html @@ -25,8 +25,8 @@

{{ course.subtitle }}

- {% if course.sessions|length > 1 %} - Lekce {{ loop.index }} – + {% if session.serial != None %} + Lekce {{ session.serial }} – {% endif %} {{ session.title }} From bb716274f2fe6c22923d4487151c4067329b84b3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 24 Feb 2019 12:32:55 +0100 Subject: [PATCH 7/9] Test dumping with API version 0.1 --- test_naucse/conftest.py | 9 +- ...ex-course.yml => complex-course.v0.0.yaml} | 0 .../expected-dumps/complex-course.v0.1.yaml | 82 +++++++++++++++++++ .../{empty-root.yml => empty-root.v0.0.yaml} | 0 .../expected-dumps/empty-root.v0.1.yaml | 8 ++ ...al-course.yml => minimal-course.v0.0.yaml} | 0 .../expected-dumps/minimal-course.v0.1.yaml | 12 +++ ...inimal-root.yml => minimal-root.v0.0.yaml} | 0 .../expected-dumps/minimal-root.v0.1.yaml | 17 ++++ ...al-course.yaml => normal-course.v0.0.yaml} | 0 .../expected-dumps/normal-course.v0.1.yaml | 24 ++++++ ...th-times.yaml => run-with-times.v0.0.yaml} | 0 .../expected-dumps/run-with-times.v0.1.yaml | 33 ++++++++ .../run-years/{2017.yml => 2017.v0.0.yaml} | 0 .../expected-dumps/run-years/2017.v0.1.yaml | 7 ++ .../run-years/{2018.yml => 2018.v0.0.yaml} | 0 .../expected-dumps/run-years/2018.v0.1.yaml | 7 ++ .../run-years/{2019.yml => 2019.v0.0.yaml} | 0 .../expected-dumps/run-years/2019.v0.1.yaml | 9 ++ .../run-years/{root.yml => root.v0.0.yaml} | 0 .../expected-dumps/run-years/root.v0.1.yaml | 23 ++++++ ...ult-times => with-default-times.v0.0.yaml} | 0 .../with-default-times.v0.1.yaml | 72 ++++++++++++++++ ...{without-dates => without-dates.v0.0.yaml} | 0 .../session-times/without-dates.v0.1.yaml | 22 +++++ ...lt-time => without-default-time.v0.0.yaml} | 0 .../without-default-time.v0.1.yaml | 66 +++++++++++++++ test_naucse/test_arca_render.py | 11 ++- test_naucse/test_course.py | 11 ++- test_naucse/test_model.py | 18 ++-- test_naucse/test_session_times.py | 7 +- 31 files changed, 411 insertions(+), 27 deletions(-) rename test_naucse/fixtures/expected-dumps/{complex-course.yml => complex-course.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/complex-course.v0.1.yaml rename test_naucse/fixtures/expected-dumps/{empty-root.yml => empty-root.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/empty-root.v0.1.yaml rename test_naucse/fixtures/expected-dumps/{minimal-course.yml => minimal-course.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/minimal-course.v0.1.yaml rename test_naucse/fixtures/expected-dumps/{minimal-root.yml => minimal-root.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/minimal-root.v0.1.yaml rename test_naucse/fixtures/expected-dumps/{normal-course.yaml => normal-course.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/normal-course.v0.1.yaml rename test_naucse/fixtures/expected-dumps/{run-with-times.yaml => run-with-times.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/run-with-times.v0.1.yaml rename test_naucse/fixtures/expected-dumps/run-years/{2017.yml => 2017.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/run-years/2017.v0.1.yaml rename test_naucse/fixtures/expected-dumps/run-years/{2018.yml => 2018.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/run-years/2018.v0.1.yaml rename test_naucse/fixtures/expected-dumps/run-years/{2019.yml => 2019.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/run-years/2019.v0.1.yaml rename test_naucse/fixtures/expected-dumps/run-years/{root.yml => root.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/run-years/root.v0.1.yaml rename test_naucse/fixtures/expected-dumps/session-times/{with-default-times => with-default-times.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/session-times/with-default-times.v0.1.yaml rename test_naucse/fixtures/expected-dumps/session-times/{without-dates => without-dates.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/session-times/without-dates.v0.1.yaml rename test_naucse/fixtures/expected-dumps/session-times/{without-default-time => without-default-time.v0.0.yaml} (100%) create mode 100644 test_naucse/fixtures/expected-dumps/session-times/without-default-time.v0.1.yaml diff --git a/test_naucse/conftest.py b/test_naucse/conftest.py index af963d9e42..3c3b7c6513 100644 --- a/test_naucse/conftest.py +++ b/test_naucse/conftest.py @@ -82,8 +82,13 @@ def assert_yaml_dump(data, filename): assert data == expected -def assert_model_dump(model, filename): - assert_yaml_dump(models.dump(model, version=[0, 0]), filename) +@pytest.fixture(params=((0, 0), (0, 1))) +def assert_model_dump(request): + version = request.param + def _assert(model, filename): + filename += '.v{}.{}.yaml'.format(*version) + assert_yaml_dump(models.dump(model, version=version), filename) + return _assert def add_test_course(model, slug, data): diff --git a/test_naucse/fixtures/expected-dumps/complex-course.yml b/test_naucse/fixtures/expected-dumps/complex-course.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/complex-course.yml rename to test_naucse/fixtures/expected-dumps/complex-course.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/complex-course.v0.1.yaml b/test_naucse/fixtures/expected-dumps/complex-course.v0.1.yaml new file mode 100644 index 0000000000..6857b5dce1 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/complex-course.v0.1.yaml @@ -0,0 +1,82 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + default_time: + end: '22:00' + start: 08:00 + description: blah + end_date: '2010-10-20' + lessons: + test/lesson1: + pages: + index: + attribution: + - me + license: cc0 + modules: {} + solutions: [] + title: A page + url: http://dummy.test/model/web/Page/?course_slug=courses/complex&lesson_slug=test/lesson1&page_slug=index + static_files: {} + url: http://dummy.test/model/web/Lesson/?course_slug=courses/complex&lesson_slug=test/lesson1 + test/lesson2: + pages: + index: + attribution: + - me + license: cc0 + modules: {} + solutions: [] + title: Another page + url: http://dummy.test/model/web/Page/?course_slug=courses/complex&lesson_slug=test/lesson2&page_slug=index + static_files: {} + url: http://dummy.test/model/web/Lesson/?course_slug=courses/complex&lesson_slug=test/lesson2 + long_description: A fun course! + place: Nivnice + sessions: + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=empty + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=empty + serial: '1' + slug: empty + title: Empty session + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=empty + - date: '2010-10-20' + description: A full session! + materials: + - type: special + - external_url: https://somewhere.test/ + type: link + url: https://somewhere.test/ + - lesson_slug: test/lesson1 + type: lesson + url: http://dummy.test/model/web/Lesson/?course_slug=courses/complex&lesson_slug=test/lesson1 + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=full + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=full + serial: '2' + slug: full + time: + end: '2010-10-20 10:00:00' + start: '2010-10-20 09:00:00' + title: Full session + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=full + source_file: courses/complex/info.yml + start_date: '2010-10-20' + subtitle: Suitable for testing only. + time_description: fortnightly yesteryear + title: A complex course + url: http://dummy.test/model/web/Course/?course_slug=courses/complex + vars: + test_var: 123 diff --git a/test_naucse/fixtures/expected-dumps/empty-root.yml b/test_naucse/fixtures/expected-dumps/empty-root.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/empty-root.yml rename to test_naucse/fixtures/expected-dumps/empty-root.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/empty-root.v0.1.yaml b/test_naucse/fixtures/expected-dumps/empty-root.v0.1.yaml new file mode 100644 index 0000000000..c1748db762 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/empty-root.v0.1.yaml @@ -0,0 +1,8 @@ +$schema: http://dummy.test/schema/Root +api_version: +- 0 +- 1 +root: + licenses: {} + run_years: {} + self_study_courses: {} diff --git a/test_naucse/fixtures/expected-dumps/minimal-course.yml b/test_naucse/fixtures/expected-dumps/minimal-course.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/minimal-course.yml rename to test_naucse/fixtures/expected-dumps/minimal-course.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/minimal-course.v0.1.yaml b/test_naucse/fixtures/expected-dumps/minimal-course.v0.1.yaml new file mode 100644 index 0000000000..854d19bd46 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/minimal-course.v0.1.yaml @@ -0,0 +1,12 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + lessons: {} + long_description: '' + sessions: [] + source_file: courses/minimal/info.yml + title: A minimal course + url: http://dummy.test/model/web/Course/?course_slug=courses/minimal + vars: {} diff --git a/test_naucse/fixtures/expected-dumps/minimal-root.yml b/test_naucse/fixtures/expected-dumps/minimal-root.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/minimal-root.yml rename to test_naucse/fixtures/expected-dumps/minimal-root.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/minimal-root.v0.1.yaml b/test_naucse/fixtures/expected-dumps/minimal-root.v0.1.yaml new file mode 100644 index 0000000000..3439090085 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/minimal-root.v0.1.yaml @@ -0,0 +1,17 @@ +$schema: http://dummy.test/schema/Root +api_version: +- 0 +- 1 +root: + licenses: + cc-by-sa-40: + title: Creative Commons Attribution-ShareAlike 4.0 International + url: https://creativecommons.org/licenses/by-sa/4.0/ + cc0: + title: CC0 1.0 Universal Public Domain Dedication + url: https://creativecommons.org/publicdomain/zero/1.0/ + run_years: {} + self_study_courses: + courses/minimal: + $ref: http://dummy.test/model/api/Course/?course_slug=courses/minimal + url: http://dummy.test/model/web/Root/? diff --git a/test_naucse/fixtures/expected-dumps/normal-course.yaml b/test_naucse/fixtures/expected-dumps/normal-course.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/normal-course.yaml rename to test_naucse/fixtures/expected-dumps/normal-course.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/normal-course.v0.1.yaml b/test_naucse/fixtures/expected-dumps/normal-course.v0.1.yaml new file mode 100644 index 0000000000..2bf355ceca --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/normal-course.v0.1.yaml @@ -0,0 +1,24 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + lessons: {} + long_description: '' + sessions: + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/normal-course&page_slug=back&session_slug=normal-lesson + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/normal-course&page_slug=front&session_slug=normal-lesson + slug: normal-lesson + source_file: courses/normal-course/info.yml + title: A normal lesson + url: http://dummy.test/model/web/Session/?course_slug=courses/normal-course&session_slug=normal-lesson + source_file: courses/normal-course/info.yml + title: A plain vanilla course + url: http://dummy.test/model/web/Course/?course_slug=courses/normal-course + vars: {} diff --git a/test_naucse/fixtures/expected-dumps/run-with-times.yaml b/test_naucse/fixtures/expected-dumps/run-with-times.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/run-with-times.yaml rename to test_naucse/fixtures/expected-dumps/run-with-times.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/run-with-times.v0.1.yaml b/test_naucse/fixtures/expected-dumps/run-with-times.v0.1.yaml new file mode 100644 index 0000000000..9a79b09d50 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/run-with-times.v0.1.yaml @@ -0,0 +1,33 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + default_time: + end: '21:00' + start: '19:00' + end_date: '2000-01-01' + lessons: {} + long_description: '' + sessions: + - date: '2000-01-01' + materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=2000/run-with-times&page_slug=back&session_slug=normal-lesson + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=2000/run-with-times&page_slug=front&session_slug=normal-lesson + slug: normal-lesson + source_file: runs/2000/run-with-times/info.yml + time: + end: '2000-01-01 21:00:00' + start: '2000-01-01 19:00:00' + title: A normal lesson + url: http://dummy.test/model/web/Session/?course_slug=2000/run-with-times&session_slug=normal-lesson + source_file: runs/2000/run-with-times/info.yml + start_date: '2000-01-01' + title: Test run with scheduled times + url: http://dummy.test/model/web/Course/?course_slug=2000/run-with-times + vars: {} diff --git a/test_naucse/fixtures/expected-dumps/run-years/2017.yml b/test_naucse/fixtures/expected-dumps/run-years/2017.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/run-years/2017.yml rename to test_naucse/fixtures/expected-dumps/run-years/2017.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/run-years/2017.v0.1.yaml b/test_naucse/fixtures/expected-dumps/run-years/2017.v0.1.yaml new file mode 100644 index 0000000000..381daddb8f --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/run-years/2017.v0.1.yaml @@ -0,0 +1,7 @@ +$schema: http://dummy.test/schema/RunYear +api_version: +- 0 +- 1 +data: + 2017/multi-year: + $ref: http://dummy.test/model/api/Course/?course_slug=2017/multi-year diff --git a/test_naucse/fixtures/expected-dumps/run-years/2018.yml b/test_naucse/fixtures/expected-dumps/run-years/2018.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/run-years/2018.yml rename to test_naucse/fixtures/expected-dumps/run-years/2018.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/run-years/2018.v0.1.yaml b/test_naucse/fixtures/expected-dumps/run-years/2018.v0.1.yaml new file mode 100644 index 0000000000..381daddb8f --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/run-years/2018.v0.1.yaml @@ -0,0 +1,7 @@ +$schema: http://dummy.test/schema/RunYear +api_version: +- 0 +- 1 +data: + 2017/multi-year: + $ref: http://dummy.test/model/api/Course/?course_slug=2017/multi-year diff --git a/test_naucse/fixtures/expected-dumps/run-years/2019.yml b/test_naucse/fixtures/expected-dumps/run-years/2019.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/run-years/2019.yml rename to test_naucse/fixtures/expected-dumps/run-years/2019.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/run-years/2019.v0.1.yaml b/test_naucse/fixtures/expected-dumps/run-years/2019.v0.1.yaml new file mode 100644 index 0000000000..97c5f8412b --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/run-years/2019.v0.1.yaml @@ -0,0 +1,9 @@ +$schema: http://dummy.test/schema/RunYear +api_version: +- 0 +- 1 +data: + 2017/multi-year: + $ref: http://dummy.test/model/api/Course/?course_slug=2017/multi-year + 2019/single-session: + $ref: http://dummy.test/model/api/Course/?course_slug=2019/single-session diff --git a/test_naucse/fixtures/expected-dumps/run-years/root.yml b/test_naucse/fixtures/expected-dumps/run-years/root.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/run-years/root.yml rename to test_naucse/fixtures/expected-dumps/run-years/root.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/run-years/root.v0.1.yaml b/test_naucse/fixtures/expected-dumps/run-years/root.v0.1.yaml new file mode 100644 index 0000000000..0c4265009a --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/run-years/root.v0.1.yaml @@ -0,0 +1,23 @@ +$schema: http://dummy.test/schema/Root +api_version: +- 0 +- 1 +root: + licenses: + cc-by-sa-40: + title: Creative Commons Attribution-ShareAlike 4.0 International + url: https://creativecommons.org/licenses/by-sa/4.0/ + cc0: + title: CC0 1.0 Universal Public Domain Dedication + url: https://creativecommons.org/publicdomain/zero/1.0/ + run_years: + 2017: + $ref: http://dummy.test/model/api/RunYear/?year=2017 + 2018: + $ref: http://dummy.test/model/api/RunYear/?year=2018 + 2019: + $ref: http://dummy.test/model/api/RunYear/?year=2019 + self_study_courses: + courses/minimal: + $ref: http://dummy.test/model/api/Course/?course_slug=courses/minimal + url: http://dummy.test/model/web/Root/? diff --git a/test_naucse/fixtures/expected-dumps/session-times/with-default-times b/test_naucse/fixtures/expected-dumps/session-times/with-default-times.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/session-times/with-default-times rename to test_naucse/fixtures/expected-dumps/session-times/with-default-times.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/session-times/with-default-times.v0.1.yaml b/test_naucse/fixtures/expected-dumps/session-times/with-default-times.v0.1.yaml new file mode 100644 index 0000000000..0ca3109595 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/session-times/with-default-times.v0.1.yaml @@ -0,0 +1,72 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + default_time: + end: '21:00' + start: '19:00' + end_date: '2000-01-01' + lessons: {} + long_description: '' + sessions: + - date: '2000-01-01' + materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=back&session_slug=normal-session + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=front&session_slug=normal-session + serial: '1' + slug: normal-session + time: + end: '2000-01-01 21:00:00' + start: '2000-01-01 19:00:00' + title: A normal session + url: http://dummy.test/model/web/Session/?course_slug=courses/with-default-times&session_slug=normal-session + - date: '2000-01-01' + materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=back&session_slug=afterparty + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=front&session_slug=afterparty + serial: '2' + slug: afterparty + time: + end: '2000-01-01 23:00:00' + start: '2000-01-01 22:00:00' + title: Afterparty (with overridden time) + url: http://dummy.test/model/web/Session/?course_slug=courses/with-default-times&session_slug=afterparty + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=back&session_slug=self-study + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=front&session_slug=self-study + serial: '3' + slug: self-study + title: Self-study (no date) + url: http://dummy.test/model/web/Session/?course_slug=courses/with-default-times&session_slug=self-study + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=back&session_slug=morning-meditation + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/with-default-times&page_slug=front&session_slug=morning-meditation + serial: '4' + slug: morning-meditation + title: Umm... no date, but a time + url: http://dummy.test/model/web/Session/?course_slug=courses/with-default-times&session_slug=morning-meditation + start_date: '2000-01-01' + title: Test course with default times + url: http://dummy.test/model/web/Course/?course_slug=courses/with-default-times + vars: {} diff --git a/test_naucse/fixtures/expected-dumps/session-times/without-dates b/test_naucse/fixtures/expected-dumps/session-times/without-dates.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/session-times/without-dates rename to test_naucse/fixtures/expected-dumps/session-times/without-dates.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/session-times/without-dates.v0.1.yaml b/test_naucse/fixtures/expected-dumps/session-times/without-dates.v0.1.yaml new file mode 100644 index 0000000000..aee9273489 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/session-times/without-dates.v0.1.yaml @@ -0,0 +1,22 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + lessons: {} + long_description: '' + sessions: + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-dates&page_slug=back&session_slug=normal-session + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-dates&page_slug=front&session_slug=normal-session + slug: normal-session + title: A normal session + url: http://dummy.test/model/web/Session/?course_slug=courses/without-dates&session_slug=normal-session + title: A plain vanilla course + url: http://dummy.test/model/web/Course/?course_slug=courses/without-dates + vars: {} diff --git a/test_naucse/fixtures/expected-dumps/session-times/without-default-time b/test_naucse/fixtures/expected-dumps/session-times/without-default-time.v0.0.yaml similarity index 100% rename from test_naucse/fixtures/expected-dumps/session-times/without-default-time rename to test_naucse/fixtures/expected-dumps/session-times/without-default-time.v0.0.yaml diff --git a/test_naucse/fixtures/expected-dumps/session-times/without-default-time.v0.1.yaml b/test_naucse/fixtures/expected-dumps/session-times/without-default-time.v0.1.yaml new file mode 100644 index 0000000000..1b0cb0c0a7 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/session-times/without-default-time.v0.1.yaml @@ -0,0 +1,66 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + end_date: '2000-01-01' + lessons: {} + long_description: '' + sessions: + - date: '2000-01-01' + materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=back&session_slug=normal-session + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=front&session_slug=normal-session + serial: '1' + slug: normal-session + title: A normal session + url: http://dummy.test/model/web/Session/?course_slug=courses/without-default-time&session_slug=normal-session + - date: '2000-01-01' + materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=back&session_slug=afterparty + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=front&session_slug=afterparty + serial: '2' + slug: afterparty + time: + end: '2000-01-01 23:00:00' + start: '2000-01-01 22:00:00' + title: Afterparty (with overridden time) + url: http://dummy.test/model/web/Session/?course_slug=courses/without-default-time&session_slug=afterparty + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=back&session_slug=self-study + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=front&session_slug=self-study + serial: '3' + slug: self-study + title: Self-study (no date) + url: http://dummy.test/model/web/Session/?course_slug=courses/without-default-time&session_slug=self-study + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=back&session_slug=morning-meditation + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/without-default-time&page_slug=front&session_slug=morning-meditation + serial: '4' + slug: morning-meditation + title: Umm... no date, but a time + url: http://dummy.test/model/web/Session/?course_slug=courses/without-default-time&session_slug=morning-meditation + start_date: '2000-01-01' + title: Test course without scheduled times + url: http://dummy.test/model/web/Course/?course_slug=courses/without-default-time + vars: {} diff --git a/test_naucse/test_arca_render.py b/test_naucse/test_arca_render.py index 2a97e80584..e8da3d64f0 100644 --- a/test_naucse/test_arca_render.py +++ b/test_naucse/test_arca_render.py @@ -13,7 +13,6 @@ from naucse.arca_renderer import RemoteRepoError from test_naucse.conftest import fixture_path, make_model, get_local_repo_info -from test_naucse.conftest import assert_model_dump def run(args, *, cwd, check=True, env=None, **kwargs): @@ -74,14 +73,14 @@ def arca_model(tmp_path, content_repo): return model -def test_valid_fork(arca_model, content_repo): +def test_valid_fork(arca_model, content_repo, assert_model_dump): """Valid data can be loaded from a Git repository""" course = models.Course.load_remote( 'courses/normal-course', parent=arca_model, link_info={'repo': content_repo.as_uri()}, ) arca_model.add_course(course) - assert_model_dump(course, 'normal-course.yaml') + assert_model_dump(course, 'normal-course') def test_yaml_error(arca_model, content_repo, git_command): @@ -181,11 +180,11 @@ def test_removed_data(arca_model, content_repo, git_command): LINK_INFO = { 'courses/normal-course': { 'path': 'courses/normal-course', - 'expected_file': 'normal-course.yaml', + 'expected_file': 'normal-course', }, '2000/run-with-times': { 'path': 'runs/2000/run-with-times', - 'expected_file': 'run-with-times.yaml', + 'expected_file': 'run-with-times', }, } @@ -203,7 +202,7 @@ def make_data_with_fork_link(tmp_path, course_path, link_content): @pytest.mark.parametrize('slug', LINK_INFO) -def test_fork_link(arca_model, content_repo, tmp_path, slug): +def test_fork_link(arca_model, content_repo, tmp_path, slug, assert_model_dump): """Test data is loaded via link.yml pointing to a repository""" link_info = {'repo': content_repo.as_uri(), 'branch': 'master'} diff --git a/test_naucse/test_course.py b/test_naucse/test_course.py index 9cbd1c5cbf..13b6f297d8 100644 --- a/test_naucse/test_course.py +++ b/test_naucse/test_course.py @@ -7,8 +7,7 @@ from naucse import models from naucse.edit_info import get_local_repo_info -from test_naucse.conftest import assert_model_dump, add_test_course -from test_naucse.conftest import fixture_path +from test_naucse.conftest import add_test_course, fixture_path class DummyRenderer: """Renderer that returns courses/lessons from the given data @@ -98,7 +97,7 @@ def test_get_lesson_url_freeze_error(empty_course): empty_course.freeze() -def test_empty_course_from_renderer(model): +def test_empty_course_from_renderer(model, assert_model_dump): """Valid trvial json that could come from a fork is loaded correctly""" source = 'courses/minimal/info.yml' renderer = DummyRenderer( @@ -118,7 +117,7 @@ def test_empty_course_from_renderer(model): renderer=renderer, ) check_empty_course_attrs(course, source_file=Path(source)) - assert_model_dump(course, 'minimal-course.yml') + assert_model_dump(course, 'minimal-course') def load_course_from_fixture(model, filename): @@ -139,11 +138,11 @@ def load_course_from_fixture(model, filename): return course -def test_complex_course(model): +def test_complex_course(model, assert_model_dump): """Valid complex json that could come from a fork is loaded correctly""" course = load_course_from_fixture(model, 'course-data/complex-course.yml') - assert_model_dump(course, 'complex-course.yml') + assert_model_dump(course, 'complex-course') # Make sure HTML is sanitized assert course.long_description == 'A fun course!' diff --git a/test_naucse/test_model.py b/test_naucse/test_model.py index fee4a1bf70..287f22a83a 100644 --- a/test_naucse/test_model.py +++ b/test_naucse/test_model.py @@ -8,7 +8,7 @@ from naucse.edit_info import get_local_repo_info from test_naucse.conftest import fixture_path, dummy_schema_url_factory -from test_naucse.conftest import assert_model_dump, add_test_course +from test_naucse.conftest import add_test_course def test_empty_model(): @@ -36,9 +36,9 @@ def test_licenses(): assert model.licenses['cc-by-sa-40'].title.endswith('4.0 International') -def test_dump_empty_model(): +def test_dump_empty_model(assert_model_dump): model = models.Root(schema_url_factory=dummy_schema_url_factory) - assert_model_dump(model, 'empty-root.yml') + assert_model_dump(model, 'empty-root') def test_load_empty_dir(): @@ -92,7 +92,7 @@ def test_add_local_course(): assert model.courses['courses/minimal'].slug == 'courses/minimal' -def test_dump_local_course(model): +def test_dump_local_course(model, assert_model_dump): path = fixture_path / 'minimal-courses' model.add_course(models.Course.load_local( parent=model, @@ -101,9 +101,9 @@ def test_dump_local_course(model): slug='courses/minimal', )) - assert_model_dump(model, 'minimal-root.yml') + assert_model_dump(model, 'minimal-root') course = model.courses['courses/minimal'] - assert_model_dump(course, 'minimal-course.yml') + assert_model_dump(course, 'minimal-course') def test_add_course_from_data(): @@ -120,7 +120,7 @@ def test_add_course_from_data(): assert model.courses['courses/minimal'].slug == 'courses/minimal' -def test_run_years(model): +def test_run_years(model, assert_model_dump): assert model.run_years == {} # Add a self-study course. It should not appear in run_years. @@ -195,6 +195,6 @@ def test_run_years(model): '2019/single-session': course_2019, } - assert_model_dump(model, 'run-years/root.yml') + assert_model_dump(model, 'run-years/root') for year, run_year in model.run_years.items(): - assert_model_dump(run_year, f'run-years/{year}.yml') + assert_model_dump(run_year, f'run-years/{year}') diff --git a/test_naucse/test_session_times.py b/test_naucse/test_session_times.py index 2f756192cf..b8bf3e36bb 100644 --- a/test_naucse/test_session_times.py +++ b/test_naucse/test_session_times.py @@ -6,7 +6,6 @@ from naucse import models from test_naucse.conftest import fixture_path, add_test_course -from test_naucse.conftest import assert_model_dump TZINFO = dateutil.tz.gettz('Europe/Prague') @@ -36,7 +35,7 @@ ] -def test_run_with_default_times(model): +def test_run_with_default_times(model, assert_model_dump): add_test_course(model, 'courses/with-default-times', { 'title': 'Test course with default times', 'default_time': {'start': '19:00', 'end': '21:00'}, @@ -74,7 +73,7 @@ def test_run_with_default_times(model): assert_model_dump(course, 'session-times/with-default-times') -def test_course_with_no_default_time(model): +def test_course_with_no_default_time(model, assert_model_dump): add_test_course(model, 'courses/without-default-time', { 'title': 'Test course without scheduled times', 'sessions': SESSIONS, @@ -105,7 +104,7 @@ def test_course_with_no_default_time(model): assert_model_dump(course, 'session-times/without-default-time') -def test_course_without_dates(model): +def test_course_without_dates(model, assert_model_dump): add_test_course(model, 'courses/without-dates', { 'title': 'A plain vanilla course', 'sessions': [ From 0dd27e118006a856771fceb0a6730416215cbca4 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 24 Feb 2019 12:38:37 +0100 Subject: [PATCH 8/9] Test loading from API 0.1 with session serials --- .../fixtures/course-data/course-v0.1.yml | 15 ++++++ .../expected-dumps/course-v0.1.v0.0.yaml | 46 ++++++++++++++++++ .../expected-dumps/course-v0.1.v0.1.yaml | 48 +++++++++++++++++++ test_naucse/test_course.py | 7 +++ 4 files changed, 116 insertions(+) create mode 100644 test_naucse/fixtures/course-data/course-v0.1.yml create mode 100644 test_naucse/fixtures/expected-dumps/course-v0.1.v0.0.yaml create mode 100644 test_naucse/fixtures/expected-dumps/course-v0.1.v0.1.yaml diff --git a/test_naucse/fixtures/course-data/course-v0.1.yml b/test_naucse/fixtures/course-data/course-v0.1.yml new file mode 100644 index 0000000000..49941a8f16 --- /dev/null +++ b/test_naucse/fixtures/course-data/course-v0.1.yml @@ -0,0 +1,15 @@ +course: + api_version: [0, 1] + course: + title: A course loaded from API version 0.1 + subtitle: Suitable for testing only. + source_file: courses/complex/info.yml + sessions: + - slug: first + title: First + serial: '1' + - slug: second + title: Second + serial: '2' + - slug: special + title: Special diff --git a/test_naucse/fixtures/expected-dumps/course-v0.1.v0.0.yaml b/test_naucse/fixtures/expected-dumps/course-v0.1.v0.0.yaml new file mode 100644 index 0000000000..1e4ad217f0 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/course-v0.1.v0.0.yaml @@ -0,0 +1,46 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 0 +course: + lessons: {} + long_description: '' + sessions: + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=first + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=first + slug: first + title: First + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=first + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=second + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=second + slug: second + title: Second + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=second + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=special + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=special + slug: special + title: Special + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=special + source_file: courses/complex/info.yml + subtitle: Suitable for testing only. + title: A course loaded from API version 0.1 + url: http://dummy.test/model/web/Course/?course_slug=courses/complex + vars: {} diff --git a/test_naucse/fixtures/expected-dumps/course-v0.1.v0.1.yaml b/test_naucse/fixtures/expected-dumps/course-v0.1.v0.1.yaml new file mode 100644 index 0000000000..ec0c14d8b8 --- /dev/null +++ b/test_naucse/fixtures/expected-dumps/course-v0.1.v0.1.yaml @@ -0,0 +1,48 @@ +$schema: http://dummy.test/schema/Course +api_version: +- 0 +- 1 +course: + lessons: {} + long_description: '' + sessions: + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=first + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=first + serial: '1' + slug: first + title: First + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=first + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=second + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=second + serial: '2' + slug: second + title: Second + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=second + - materials: [] + pages: + back: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=back&session_slug=special + front: + content: '' + url: http://dummy.test/model/web/SessionPage/?course_slug=courses/complex&page_slug=front&session_slug=special + slug: special + title: Special + url: http://dummy.test/model/web/Session/?course_slug=courses/complex&session_slug=special + source_file: courses/complex/info.yml + subtitle: Suitable for testing only. + title: A course loaded from API version 0.1 + url: http://dummy.test/model/web/Course/?course_slug=courses/complex + vars: {} diff --git a/test_naucse/test_course.py b/test_naucse/test_course.py index 13b6f297d8..4c6cc20c3a 100644 --- a/test_naucse/test_course.py +++ b/test_naucse/test_course.py @@ -149,6 +149,13 @@ def test_complex_course(model, assert_model_dump): assert course.sessions['full'].description == 'A full session!' +def test_api_1_0(model, assert_model_dump): + """Valid complex json that could come from a fork is loaded correctly""" + course = load_course_from_fixture(model, 'course-data/course-v0.1.yml') + + assert_model_dump(course, 'course-v0.1') + + def test_derives(model): """Test that derives and base_course is set correctly""" add_test_course(model, 'courses/base', { From ee9b4bc6ac7833c4a7839f9aa55e36092508d3df Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Sun, 24 Feb 2019 13:16:21 +0100 Subject: [PATCH 9/9] views: Use version 0.1 in the exported API --- naucse/models.py | 2 ++ naucse/views.py | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/naucse/models.py b/naucse/models.py index 34900bbc4d..96d8754c30 100644 --- a/naucse/models.py +++ b/naucse/models.py @@ -20,6 +20,8 @@ import naucse_render +API_VERSION = 0, 1 + # XXX: Different timezones? _TIMEZONE = 'Europe/Prague' diff --git a/naucse/views.py b/naucse/views.py index f72bfdbd82..1f9c221ed7 100644 --- a/naucse/views.py +++ b/naucse/views.py @@ -399,12 +399,14 @@ def schema(model_slug, is_input): cls = models.models[model_slug] except KeyError: abort(404) - return jsonify(models.get_schema(cls, is_input=is_input)) + return jsonify(models.get_schema( + cls, is_input=is_input, version=models.API_VERSION, + )) @app.route('/v0/naucse.json') def api(): - return jsonify(models.dump(g.model)) + return jsonify(models.dump(g.model, version=models.API_VERSION)) @app.route('/v0/years/.json') @@ -413,7 +415,7 @@ def run_year_api(year): run_year = g.model.run_years[year] except KeyError: abort(404) - return jsonify(models.dump(run_year)) + return jsonify(models.dump(run_year, version=models.API_VERSION)) @app.route('/v0/.json') @@ -422,4 +424,4 @@ def course_api(course_slug): course = g.model.courses[course_slug] except KeyError: abort(404) - return jsonify(models.dump(course)) + return jsonify(models.dump(course, version=models.API_VERSION))