A basic model

To start off, we first build some basic concepts from our planned model. This gives a foundation onto which all the rest can be built, and which can be extended as we go along. It is a good idea to write a simple test that covers some facts explaining the model before the actual code is written.

Since we’ve been through writing tests for Addresses in the previous examples, those will be skipped here. The tests below focus on the the collaborators and rights aspects introduced by this example. Previously an AddressBook was not necessary, since all Addresses were seen to be in a single AddressBook – the database itself. This time around each Address belongs to a specific AddressBook, so the concept of an AddressBook has to be mentioned in our new tests:

# To run this test do:
# nosetests -F reahl.webdev.fixtures:BrowserSetup -s --nologcapture reahl/doc_dev/tutorialtests/accesstests1.py


from __future__ import print_function, unicode_literals, absolute_import, division
from reahl.tofu import test
from reahl.web_dev.fixtures import WebFixture
from reahl.domain.systemaccountmodel import EmailAndPasswordSystemAccount

from reahl.sqlalchemysupport import Session

from reahl.doc.examples.tutorial.access1.access1 import AddressBook, Address


class AccessFixture(WebFixture):
    password = 'bobbejaan'

    def new_account(self, email='[email protected]'):
        account = EmailAndPasswordSystemAccount(email=email)
        Session.add(account)
        account.set_new_password(account.email, self.password)
        account.activate()
        return account

    def new_address_book(self, owner=None):
        owner = owner or self.account
        address_book = AddressBook(owner=owner)
        Session.add(address_book)
        return address_book

    def new_other_account(self):
        return self.new_account(email='[email protected]')
        
    def new_other_address_book(self):
        return self.new_address_book(owner=self.other_account)



@test(AccessFixture)
def separate_address_books(fixture):
    """An Address is created in a particular AddressBook, which is owned by a SystemAccount."""
    
    account = fixture.account
    address_book = fixture.address_book
    other_address_book = fixture.other_address_book

    # AddressBooks are owned
    address_book.owner is account
    other_address_book.owner is fixture.other_account

    # Addresses live in specific AddressBooks
    assert address_book.addresses == []
    assert other_address_book.addresses == []

    address1 = Address(address_book=address_book, email_address='[email protected]', name='Friend1')
    address2 = Address(address_book=address_book, email_address='[email protected]', name='Friend2')

    address3 = Address(address_book=other_address_book, email_address='[email protected]', name='Friend3')

    for address in [address1, address2, address3]:
        address.save()

    assert address_book.addresses == [address1, address2]
    assert other_address_book.addresses == [address3]


@test(AccessFixture)
def collaborators(fixture):
    """A particular SystemAccount can see its own AddressBooks as well as all the AddressBooks
       it is explicitly allowed to see, but no other AddressBooks."""
    
    account = fixture.account
    address_book = fixture.address_book
    other_address_book = fixture.other_address_book

    unrelated_account = fixture.new_account(email='[email protected]')
    unrelated_address_book = fixture.new_address_book(owner=unrelated_account)
    
    other_address_book.allow(account)
    
    # Checks to see whether an AddressBook is visible
    assert address_book.is_visible_to(account)
    assert other_address_book.is_visible_to(account)
    assert not unrelated_address_book.is_visible_to(account)

    # Getting a list of visible AddressBooks (for populating the screen)
    books = AddressBook.address_books_visible_to(account)
    assert set(books) == {address_book, other_address_book}


@test(AccessFixture)
def collaborator_rights(fixture):
    """When allowing an account to see another's AddressBook, the rights it has to the AddressBook are specified."""

    account = fixture.account
    address_book = fixture.address_book
    other_address_book = fixture.other_address_book

    # Case: defaults
    other_address_book.allow(account)
    assert not other_address_book.can_be_edited_by(account)
    assert not other_address_book.can_be_added_to_by(account)

    # Case: rights specified
    other_address_book.allow(account, can_edit_addresses=True, can_add_addresses=True)
    assert other_address_book.can_be_edited_by(account)
    assert other_address_book.can_be_added_to_by(account)


@test(AccessFixture)
def adding_collaborators(fixture):
    """The owner of an AddressBook may add collaborators to it."""

    account = fixture.account
    address_book = fixture.address_book
    other_address_book = fixture.other_address_book

    assert address_book.collaborators_can_be_added_by(account)
    assert not other_address_book.collaborators_can_be_added_by(account)

After writing the test (and copying some code from the previous examples), we’re ready to write the model code that make the tests pass.

from __future__ import print_function, unicode_literals, absolute_import, division

from sqlalchemy import Column, ForeignKey, Integer, UnicodeText, Boolean
from sqlalchemy.orm import relationship, backref

from reahl.sqlalchemysupport import Session, Base
from reahl.domain.systemaccountmodel import EmailAndPasswordSystemAccount


class Address(Base):
    __tablename__ = 'access1_address'

    id              = Column(Integer, primary_key=True)    
    address_book_id = Column(Integer, ForeignKey('access1_address_book.id'))
    address_book    = relationship('reahl.doc.examples.tutorial.access1.access1.AddressBook')
    email_address   = Column(UnicodeText)
    name            = Column(UnicodeText)

    def save(self):
        Session.add(self)


class AddressBook(Base):
    __tablename__ = 'access1_address_book'

    id              = Column(Integer, primary_key=True)

    owner_id   = Column(Integer, ForeignKey(EmailAndPasswordSystemAccount.id), nullable=False)
    owner      = relationship(EmailAndPasswordSystemAccount)
    collaborators = relationship('reahl.doc.examples.tutorial.access1.access1.Collaborator', lazy='dynamic',
                                 backref='address_book')

    @classmethod
    def owned_by(cls, account):
        return Session.query(cls).filter_by(owner=account)
        
    @classmethod
    def address_books_visible_to(cls, account):
        visible_books = Session.query(cls).join(Collaborator).filter(Collaborator.account == account).all()
        visible_books.extend(cls.owned_by(account))
        return visible_books

    @property
    def addresses(self):
        return Session.query(Address).filter_by(address_book=self).all()

    def allow(self, account, can_add_addresses=False, can_edit_addresses=False):
        Session.query(Collaborator).filter_by(address_book=self, account=account).delete()
        Collaborator(address_book=self, account=account,
            can_add_addresses=can_add_addresses,
            can_edit_addresses=can_edit_addresses)

    def can_be_edited_by(self, account):
        if account is self.owner: 
            return True
            
        collaborator = self.get_collaborator(account)
        return (collaborator and collaborator.can_edit_addresses) or self.can_be_added_to_by(account)

    def can_be_added_to_by(self, account):
        if account is self.owner: 
            return True

        collaborator = self.get_collaborator(account)
        return collaborator and collaborator.can_add_addresses
        
    def collaborators_can_be_added_by(self, account):
        return self.owner is account

    def is_visible_to(self, account):
        return self in self.address_books_visible_to(account)

    def get_collaborator(self, account):            
        collaborators = self.collaborators.filter_by(account=account)
        count = collaborators.count()
        if count == 1:
            return collaborators.one()
        if count > 1:
            raise ProgrammerError('There can be only one Collaborator per account. Here is more than one.')
        return None


class Collaborator(Base):
    __tablename__ = 'access1_collaborator'
    id      = Column(Integer, primary_key=True)

    address_book_id = Column(Integer, ForeignKey(AddressBook.id))

    account_id = Column(Integer, ForeignKey(EmailAndPasswordSystemAccount.id), nullable=False)
    account = relationship(EmailAndPasswordSystemAccount)
    
    can_add_addresses = Column(Boolean, default=False)
    can_edit_addresses = Column(Boolean, default=False)

Previous topic

Security and access control

Next topic

The user interface – without access control