Extend an existing Blok

In this chapter we will discover how to add a field to the ̀ `Address`` model.

Note: You can use III-01_external-blok directory from AnyBlok/anyblok-book-examples repository to get ready to start with this chapter.

Clean up examples

Before extending the anyblok_address in the next chapter let's do some clean-up and remove examples generated by cookiecutter template in the previous section

  • Start by removing the following files:
rm rooms_booking/room/model.py \
   rooms_booking/room/tests/test_model.py \
   rooms_booking/room/tests/test_pyramid.py \
   rooms_booking/room/views.py
  • Remove pyramid configurations:
     # file: ``/rooms_booking/room/__init__.py``

-    @classmethod
-    def pyramid_load_config(cls, config):
-        config.add_route("root", "/")
-        config.add_route("example_list", "/example")
-        config.add_route("example", "/example/{id}")
-        config.scan(cls.__module__ + '.views')

Extend the Address model

We need to add an access information field on each address. Let's add two new files in your project:

  • rooms_booking/room/address.py: We are going to extend Address model in this python module
  • rooms_booking/room/tests/test_address.py: here we are going to test our additional features on this model.

In order to import the address python module (the address.py file), in an Anyblok project you must do it in the import declaration module method and the reload declaration module method in the Blok class declaration.

    # file: ``/rooms_booking/room/__init__.py``
    # class: Room(Blok)

    @classmethod
    def import_declaration_module(cls):
        """Python module to import bloks in the given order at start-up
        """
        from . import address  # noqa

    @classmethod
    def reload_declaration_module(cls, reload):
        """Python module to import while reloading server (i.e. when
        adding Blok at runtime)
        """
        from . import address  # noqa
        reload(address)

Those methods are called when the registry is created or reloaded for a given database.

In our case if the Room blok state is installed in a database address.py will be imported. Otherwise all our code will be present but not used because the blok is uninstalled.

In the same file, adapt the update method. At the moment we are not going to do anything here while updating or installing the Room Blok:

    # file: ``/rooms_booking/room/__init__.py``
    # class: Room(Blok)

    def update(self, latest_version):
         """Update blok"""
         # if we install this blok in the database we add a new record
        if not latest_version:
            self.install()

    def install(self):
        pass

Note: When you need to know if a blok is installed, launch an anyblok_interpreter and query registry.System.Blok

In [1]: registry.System.Blok.query().all()
Out[1]: 
[anyblok-core (installed),
 address (installed),
 auth (uninstalled),
 auth-password (uninstalled),
 authorization (uninstalled),
 model_authz (uninstalled),
 room (installed),
 anyblok-test (uninstalled)]

In [2]: exit

(loving TDD) Before you start coding, add the following unit tests. This way, we can test that we can add some access information on an Address:

# file: rooms_booking/room/tests/test_address.py


class TestAddress:
    """Test extended registry.Address model"""

    def test_create_address(self, rollback_registry):
        registry = rollback_registry
        address_count = registry.Address.query().count()
        queens_college_address = registry.Address.insert(
            first_name="The Queen's College",
            last_name="University of oxford",
            street1="High Street",
            zip_code="OX1 4AW",
            city="Oxford",
            country="GBR",
            access="Kick the door to open it!"
        )
        assert registry.Address.query().count() == address_count + 1
        assert queens_college_address.access == "Kick the door to open it!"

Before starting tests, you may need to create test database using make setup-tests and then run anyblok_updatedb -c app.test.cfg --install-bloks address in order to install address blok if it is not already.

If you run this test you'll probably notice the following error as we haven't created the access field on our Address model yet.

make setup-tests
make test
ANYBLOK_CONFIG_FILE=app.test.cfg py.test -v -s rooms_booking
========================================== test session starts ==========================================
platform linux -- Python 3.5.3, pytest-4.6.3, py-1.8.0, pluggy-0.12.0 -- ~/anyblok/venvs/book/bin/python3
cachedir: .pytest_cache
rootdir: ~/anyblok/anyblok-book-examples, inifile: tox.ini
plugins: cov-2.7.1
collected 1 item                                                                                        

rooms_booking/room/tests/test_address.py::TestAddress::test_create_address AnyBlok Load init: EntryPoint.parse('anyblok_pyramid_config = anyblok_pyramid:anyblok_init_config')
Loading config file '/etc/xdg/AnyBlok/conf.cfg'
Loading config file '~/.config/AnyBlok/conf.cfg'
Loading config file '~/anyblok/anyblok-book-examples/app.test.cfg'
Loading config file '~/anyblok/anyblok-book-examples/app.cfg'
Loading config file '/etc/xdg/AnyBlok/conf.cfg'
Loading config file '~/.config/AnyBlok/conf.cfg'
FAILED

=============================================== FAILURES ================================================
____________________________________ TestAddress.test_create_address ____________________________________

self = <rooms_booking.room.tests.test_address.TestAddress object at 0x7f6765e0bcc0>
rollback_registry = <anyblok.registry.Registry object at 0x7f676583fe10>

    def test_create_address(self, rollback_registry):
        registry = rollback_registry
        address_count = registry.Address.query().count()
        queens_college_address = registry.Address.insert(
            first_name="The Queen's College",
            last_name="University of oxford",
            street1="High Street",
            zip_code="OX1 4AW",
            city="Oxford",
            country="GBR",
>           access="Kick the door to open it!"
        )

rooms_booking/room/tests/test_address.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../venvs/book/lib/python3.5/site-packages/anyblok/bloks/anyblok_core/core/sqlbase.py:605: in insert
    instance = cls(**kwargs)
<string>:4: in __init__
    ???
../venvs/book/lib/python3.5/site-packages/sqlalchemy/orm/state.py:441: in _initialize_instance
    manager.dispatch.init_failure(self, args, kwargs)
../venvs/book/lib/python3.5/site-packages/sqlalchemy/util/langhelpers.py:68: in __exit__
    compat.reraise(exc_type, exc_value, exc_tb)
../venvs/book/lib/python3.5/site-packages/sqlalchemy/util/compat.py:154: in reraise
    raise value
../venvs/book/lib/python3.5/site-packages/sqlalchemy/orm/state.py:438: in _initialize_instance
    return manager.original_init(*mixed[1:], **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Address: None, None, None, None, None, Country(alpha_2='GB', alpha_3='GBR', name='United Kingdom', numeric='826', official_name='United Kingdom of Great Britain and Northern Ireland') [RO=None] >
kwargs = {'access': 'Kick the door to open it!', 'city': 'Oxford', 'country': 'GBR', 'first_name': "The Queen's College", ...}
cls_ = <class 'anyblok.model.factory.ModelAddress'>, k = 'access'

    def _declarative_constructor(self, **kwargs):
        """A simple constructor that allows initialization from kwargs.

        Sets attributes on the constructed instance using the names and
        values in ``kwargs``.

        Only keys that are present as
        attributes of the instance's class are allowed. These could be,
        for example, any mapped columns or relationships.
        """
        cls_ = type(self)
        for k in kwargs:
            if not hasattr(cls_, k):
                raise TypeError(
>                   "%r is an invalid keyword argument for %s" % (k, cls_.__name__)
                )
E               TypeError: 'access' is an invalid keyword argument for ModelAddress

../venvs/book/lib/python3.5/site-packages/sqlalchemy/ext/declarative/base.py:840: TypeError
=========================================== warnings summary ============================================
[...]
======================================== short test summary info ========================================
FAILED rooms_booking/room/tests/test_address.py::TestAddress::test_create_address - TypeError: 'access...
================================= 1 failed, 1 warnings in 2.06 seconds ==================================
Makefile:57 : la recette pour la cible « test » a échouée
make: *** [test] Erreur 1

It's time to extend the Address model to make our previous test successful:

# file: rooms_booking/room/address.py

from anyblok import Declarations
from anyblok.column import String

Model = Declarations.Model
register = Declarations.register


@register(Model)
class Address:
    """Extend and specialize anyblok_address blok"""
    access = String(label="Access information")

As you can see, it's simple to overload a model you only have to use a decorator @register(Model) on your class using the model name (here Address) and declare a new field access using a String column.

If you're wondering which columns types are supported you may read columns types in the documentation.

You can add other column type support not provided by AnyBlok Core like AnyBlok postgres package that provide JsonB support while using Postgresql database.

Your test should pass but before any further step, you have to update your blok in order to create missing fields in your test database (or re-create the database from scratch using make setup-tests):

# Be sure your project python package is installed in develop mode
pip install -e .
# Update the database model to add the access field
anyblok_updatedb -c app.test.cfg --update-bloks room
# Run unit test
make test

results matching ""

    No results matching ""