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 reahl.doc.examples.tutorial.access1.access1_dev.accesstests1
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)