From f0a73341ce9377759d6b06b9058e8a5fc6cd4574 Mon Sep 17 00:00:00 2001 From: duker33 Date: Thu, 14 Feb 2019 11:28:56 +0300 Subject: [PATCH] #240 Page views and Page context classes (#262) * #240 Process DB templates with new page view class * #240 Create page context. Move base contexts to page module side * #240 Rename module `db_views` to `display` * #240 Add test for attribute access * #240 Merge fix * #240 Merge fix #2 --- catalog/newcontext/__init__.py | 2 +- catalog/newcontext/category.py | 2 +- catalog/newcontext/context.py | 22 ++---------------- pages/display.py | 42 ++++++++++++++++++++++++++++++++++ pages/models.py | 10 ++++---- pages/newcontext/__init__.py | 3 +++ pages/newcontext/base.py | 23 +++++++++++++++++++ pages/newcontext/pages.py | 12 ++++++++++ pages/utils.py | 7 ++++-- tests/pages/test_display.py | 13 +++++++++++ tests/pages/test_models.py | 24 +++++++++++++------ 11 files changed, 124 insertions(+), 36 deletions(-) create mode 100644 pages/display.py create mode 100644 pages/newcontext/__init__.py create mode 100644 pages/newcontext/base.py create mode 100644 pages/newcontext/pages.py create mode 100644 tests/pages/test_display.py diff --git a/catalog/newcontext/__init__.py b/catalog/newcontext/__init__.py index 71305e1..755a53e 100644 --- a/catalog/newcontext/__init__.py +++ b/catalog/newcontext/__init__.py @@ -3,4 +3,4 @@ # @todo #207:15m Annotate methods of Context implementations. from . import category, products, tags -from .context import Context, Contexts, ModelContext, Products, Tags +from .context import Context, ModelContext, Products, Tags diff --git a/catalog/newcontext/category.py b/catalog/newcontext/category.py index ab8688a..2abf770 100644 --- a/catalog/newcontext/category.py +++ b/catalog/newcontext/category.py @@ -1,5 +1,5 @@ from catalog.models import AbstractCategory -from catalog.newcontext.context import Context as BaseContext +from pages.newcontext import Context as BaseContext class Context(BaseContext): diff --git a/catalog/newcontext/context.py b/catalog/newcontext/context.py index 3b84be1..d330f1e 100644 --- a/catalog/newcontext/context.py +++ b/catalog/newcontext/context.py @@ -1,14 +1,9 @@ import abc -import collections - -from catalog import typing -class Context(abc.ABC): +from catalog import typing - @abc.abstractmethod - def context(self) -> typing.Dict[str, typing.Any]: - ... +from pages.newcontext import Context class ModelContext(abc.ABC): @@ -27,19 +22,6 @@ def context(self) -> typing.Dict[str, typing.Any]: Context.register(ModelContext) -class Contexts(Context): - - # use context as list, not as args pack (`*context`) - # to move attention on homogeneous nature of args - def __init__(self, contexts: typing.List[Context]): - self.contexts = contexts or [] - - def context(self): - return dict(collections.ChainMap( - *[ctx.context() for ctx in self.contexts] - )) - - class Tags(ModelContext): def context(self): diff --git a/pages/display.py b/pages/display.py new file mode 100644 index 0000000..378894d --- /dev/null +++ b/pages/display.py @@ -0,0 +1,42 @@ +""" +Views processing db based templates. +This view are outside of MTV concept. +Responsible only for rendering given context data with db preserved text template. +""" +import typing + +from pages import models + + +class Fields: + # Fields stored in DB. See class `pages.models.PageTemplate` + STORED = ['name', 'h1', 'keywords', 'description', 'title', 'seo_text'] + + def __init__(self, page_display: 'Page'): + self.page_display = page_display + + def __getattr__(self, item): + if item in self.STORED: + return self.page_display.render(item) + else: + return super().__getattribute__(item) + + +class Page: + # @todo #240:30m Create usage doc for page view. + + def __init__(self, page: models.Page, context: typing.Dict[str, typing.Any]): + """ + Pass context at ctor, but not render method, + because client code wants the same context for many different cases. + """ + self.page = page + self.context = context + self.fields = Fields(self) + + def render(self, field: str): + return ( + self.page.template.render_field(field, self.context) + if self.page.template + else getattr(self.page, field) + ) diff --git a/pages/models.py b/pages/models.py index 169bd18..f489213 100644 --- a/pages/models.py +++ b/pages/models.py @@ -54,8 +54,8 @@ class Meta: def __str__(self): return self.name - def render(self, field, context): - return render_str(field, context) + def render_field(self, field: str, context: dict) -> str: + return render_str(getattr(self, field), context) class PageQuerySet(mptt.querysets.TreeQuerySet): @@ -211,9 +211,9 @@ def display_attribute(self, name): if not self.template: return getattr(self, name) or self.name - return self.template.render( - getattr(self.template, name), - self.get_template_render_context(), + return self.template.render_field( + field=name, + context=self.get_template_render_context(), ) @property diff --git a/pages/newcontext/__init__.py b/pages/newcontext/__init__.py new file mode 100644 index 0000000..b758344 --- /dev/null +++ b/pages/newcontext/__init__.py @@ -0,0 +1,3 @@ +from . import base, pages +from .base import Context, Contexts +from .pages import Page diff --git a/pages/newcontext/base.py b/pages/newcontext/base.py new file mode 100644 index 0000000..b33170d --- /dev/null +++ b/pages/newcontext/base.py @@ -0,0 +1,23 @@ +import abc +import collections +import typing + + +class Context(abc.ABC): + + @abc.abstractmethod + def context(self) -> typing.Dict[str, typing.Any]: + ... + + +class Contexts(Context): + + # use context as list, not as args pack (`*context`) + # to move attention on homogeneous nature of args + def __init__(self, contexts: typing.List[Context]): + self.contexts = contexts or [] + + def context(self): + return dict(collections.ChainMap( + *[ctx.context() for ctx in self.contexts] + )) diff --git a/pages/newcontext/pages.py b/pages/newcontext/pages.py new file mode 100644 index 0000000..811ab5c --- /dev/null +++ b/pages/newcontext/pages.py @@ -0,0 +1,12 @@ +from .base import Context + + +class Page(Context): + + def __init__(self, page): + self.page = page + + def context(self): + return { + 'page': self.page, + } diff --git a/pages/utils.py b/pages/utils.py index db21ff0..6bf91c8 100644 --- a/pages/utils.py +++ b/pages/utils.py @@ -3,9 +3,12 @@ from django.template import engines -def render_str(text, data): +# @todo #240:30m Create TextView class. +# Instead of render_str method. +# And inherit `pages.db_views.Page` from this class. +def render_str(template: str, context: dict): django_engine = engines['django'] - return django_engine.from_string(text).render(data) + return django_engine.from_string(template).render(context) def save_custom_pages(): diff --git a/tests/pages/test_display.py b/tests/pages/test_display.py new file mode 100644 index 0000000..9d338aa --- /dev/null +++ b/tests/pages/test_display.py @@ -0,0 +1,13 @@ +from django.test import TestCase + +from pages import display + + +class TestFields(TestCase): + fixtures = [] + + def test_attribute_error(self): + # noinspection PyTypeChecker + page = display.Page(None, {}) + with self.assertRaises(AttributeError): + _ = page.fields.bad_attr diff --git a/tests/pages/test_models.py b/tests/pages/test_models.py index e7f8659..83cd2fd 100644 --- a/tests/pages/test_models.py +++ b/tests/pages/test_models.py @@ -1,5 +1,6 @@ from django.test import TestCase +from pages import display from pages.models import ModelPage, CustomPage, FlatPage, Page, PageTemplate from tests.models import MockEntity, MockEntityWithSync @@ -100,34 +101,43 @@ def test_slug_should_auto_generate(self): self.assertTrue(page.slug) + # @todo #240:30m Improve DB templates (and views) tests. + # Move them to separated module. + # Rename theirs `test_display` prefix. + # Separate them on small pieces. + # Add test for `display.Page` with passing context. def test_display_seo_fields(self): page_with_custom_fields = Page.objects.create( name='some page', slug='test', h1='test h1' ) - self.assertEqual(page_with_custom_fields.display_h1, 'test h1') + page_view = display.Page(page_with_custom_fields, {}) + self.assertEqual(page_view.fields.h1, 'test h1') custom_page_template = PageTemplate.objects.create( name='test', h1='{{ page.name }} - купить в СПб', ) - page_with_template = Page.objects.create( + page = Page.objects.create( name='different page', template=custom_page_template ) - self.assertEqual(page_with_template.display_h1, 'different page - купить в СПб') + page_view = display.Page(page, {'page': page}) + + self.assertEqual(page_view.fields.h1, 'different page - купить в СПб') def test_display_attribute_uses_template(self): - custom_page_template = PageTemplate.objects.create( + template = PageTemplate.objects.create( name='test', h1='{{ page.h1 }} - template', ) - page_with_template = Page.objects.create( + page = Page.objects.create( name='different page', h1='page h1', - template=custom_page_template, + template=template, ) - self.assertEqual(page_with_template.display_h1, 'page h1 - template') + page_view = display.Page(page, {'page': page}) + self.assertEqual(page_view.fields.h1, 'page h1 - template') class TestCustomPage(TestCase):