Bricolage Multi-Site Feature Implementation Documentation
This document describes everything done to implement the multi-site feature in Bricolage.
Bric::Biz::Site was implemented similarly to the other Bricolage business
objects. It inherits from Bric, uses Bric::register_fields() to
register its instance attributes and to create accessors, and stores its data
in the database.
The interface for Bric::Biz::Site is nearly exactly as it was described in
MultiSite::TechnicalSpec. The only difference is
the addition of a couple of private instance methods, get_asset_grp(), and
list_priv_grps(), and the removal of the user_id parameter to list()
and friends. See Bric::Biz::Site for a complete description
of the API.
Note that, as described in MultiSite::TechnicalSpec, the IDs for all sites
correspond to a single secret group in which the site is actually a
member. This approach makes it very easy to use the site ID as a group ID as
well, in those classes that need to include a site ID in their grp_ids
attributes for permission-checking purposes. Good examples of this use include
Bric::Biz::Asset and it subclasses.
The private get_asset_grp() method returns the secret asset group that
corresponds to a site object ID. I've included it for completeness, but left
it private for now, as there has not been a public need for it as of yet.
The private list_priv_grps() method returns the four secret user groups
created by every site object. I've included it for completeness, but left it
private for now, as there has not been a public need for it as of yet
(although it seems likely that it will later be needed for displaying user
groups in the site profile). The four groups are each named
for the site plus a permission, such as ``Default Site READ Users'', and has a
Bric::Util::Priv permission object with the appropriate permission granted to
the asset group represented by the site ID and returned by get_asset_grp().
This implementation is admittedly a bit hackish, but was the simplest approach
for providing the ability to grant users to access assets within a site while
exploiting the existing group-based permission system and thus avoiding
creating a totally independent permissions system just for sites.
The user_id parameter had to be removed from list() because it proved
much to complicated to get the permissions checking correct in the SQL query,
and because it didn't allow members of the ``Global Admins'' group to
short-circuit permissions checking.
The Bric::Biz::Site data is stored in the database in the ``site'' table. The
columns correspond to the attribute names. The ``id'' column has a primary key
index, and the ``name'' and ``domain_name'' columns will each have a UNIQUE
index. There is no sequence for this table, since sites use IDs grabbed from
groups (and thus from the ``grp'' table's sequence). Thus the ``site'' table's
``id'' column also has a foreign key constraint linking it to the ``grp'' table's
ID column. See inst/upgrade/1.7.0/site.pl for all of the relevant SQL.
By default, Bricolage now contains one default site, named ``Default''. Its ID is 100. It is associated with a new permanent secret asset group, which has the same ID. It is further associated with four new permanent secret groups with the IDs 200-203. Each of those, in turn, as a permission tying it to the site's asset group (id 100), granting the appropriate permission for each group. The new permission IDs are 43-46.
A new entry has been added to the ``class'' table in the database for the new
Bric::Biz::Site class. It's key_name is ``site''.
See inst/upgrade/1.7.0/site.pl for all of the relevant SQL.
A comprehensive set of tests was added to give the new Bric::Biz::Site API a thorough working-over. The tests are all in Bric::Biz::Site::Test and Bric::Biz::Site::DevTest in the t directory. Note that these tests test the attributes, groups, and permissions of both the default site and sites they create using the API, in order to ensure parity between the default site and those created by the API (and I did catch and fix differences because of it!). I expect that these tests will become a model for the comprehensive testing of a Bricolage class, and their approach will be borrowed to create tests for all new Bricolage classes from here on in. Existing classes, too, will gradually gain more comprehensive tests modeled on these tests.
The Bric::Biz::Site interface is completely documented in POD. The POD is in a new style that varies considerable from the POD of existing Bricolage classes. This, too, will become the standard for new Bricolage classes from here on in. Existing classes will gradually have their POD styles updated to match. A thorough description of this POD style will soon be added to Bric::Hacker.
This new subclass of Bric::Util::Grp manages groups of Bric::Biz::Site objects. Site groups will not be secret by default, and the Bric::Util::Grp::Site class references the new Bric::Util::Class object ID for the Bric::Biz::Site class and the Bric::Util::Class object ID for for the Bric::Util::Grp::Site class itself.
The interface for Bric::Util::Grp::Site is directly inherited from Bric::Util::Grp.
A new table, ``site_member'', was added to the database, as was a sequence for it, ``seq_site_member'', and the appropriate foreign key indexes. See inst/upgrade/1.7.0/site.pl for all of the relevant SQL.
A new default group, ``All Sites'', has been added, and all Bric::Biz::Site objects will be members of this group.
A new entry has been added to the ``class'' table in the database for the new
Bric::Util::Grp::Site class. It's key_name is ``site_grp''.
See inst/upgrade/1.7.0/site.pl for all of the relevant SQL.
A few changes were made to Bric::Util::Grp in order to facilitate the new
dynamically-created groups that need to be both secret and permanent. First,
permanent and secret parameters to new() are now explicitly
supported. And second, the long-standing conflict between the class method
get_secret() method and the instance method get_secret() (or lack
thereof) had been eliminated by the addition of the instance method
is_secret(). Now get_secret() returns the default secret value for new
group objects, while is_secret() returns the value for an actual group
instance. The TODO test in Bric::Util::Grp::DevTest was thus changed to a
normal test.
There was only one very simple change to Bric::Biz::Person::User: its
get_grps() method was altered so that it would return all user groups,
including secret groups. This is so that it will return the secret groups
created by Bric::Biz::Site, so that they may be displayed in the user profile
and users thus easily associated with them.
No changes were made to the Bric::Util::EventType class, but new event types were added to the database, ``site_new'', ``site_save'', and ``site_deact''. See inst/upgrade/1.7.0/site_events.pl for the SQL that creates the new events.
Bric::Util::Fault had a couple new subclasses added to it.
Bric::Util::Fault::Error and its subclasses are data error exceptions. They're
designed to be thrown now when there's a fatal error, but when there's a
non-fatal error that the user can respond to. As such, it has a new attribute
and method, maketext, which takes as its argument an array reference of
arguments suitable for passing to Bric::Util::Langage->maketext.
A first subclass has been added, Bric::Util::Fault::Error::NotUnique.
Bric::Biz::Site throws this exception when it notes that a value passed to
its set_name() or set_domain_name() methods is already in use by
another site in the database -- in other words, when the value is not unique.
Bric::Biz::Site throws the exception like this:
throw_not_unique error => "Value of $disp cannot be empty",
maketext => ["Value of [_1] cannot be empty", $disp]
unless $value;
Note the use of the array reference for the maketext parameter. This allows
the UI callbacks to pass the localization arguments off to
Bric::Util::Language.
Bric::Util::Language now overrides the parent maketext() method so that it
can detect whether its arguments are passed as an array reference or as a list
and do the right thing. This is to support the new Bric::Util::Fault::Error
class' array reference maketext attribute:
eval {
...
};
if (my $err = $@) {
rethrow_exception($err) unless isa_bric_exception($err, 'Error');
add_msg($lang->maketext($err->maketext));
}
Two new methods were added to Bric::App::Cache, get_user_cx() and
set_user_cx(). These are used to get and set the site context for a given
user. They were added Bric::App::Cache because they needed to persist across
sessions and because, API-wise, sites are not too closely tied to users, so
storing the data in the database doesn't make much sense.
A full suite of tests was written for Bric::App::Cache, as well. None existed before now.
The interface for Bric::Biz::Category follows the changes called for in MultiSite::TechnicalSpec.
Removed ROOT_CATEGORY_ID constant and replaced it with methods
site_root_category and site_root_category_id. One returns the root
category object for a given site and the other just returns the category ID for
a given site. If called as a class method the site ID or site object must be
passed as an argument. If called as an instance method the site_id will be
pulled from the object itself.
One final method has been added, is_root_category which can be used to
determine if the object this is called against is a root category or not. The
criteria for this right now is that its parent_id is the ID of the master root
category. The master root category has ID 0 and all site root categories list
it as their parent. Nothing should ever use the master root category directory
as its just there to allow the parent_id field to remain NOT NULL.
The Bric::Biz::Category SQL follows the changes called for in MultiSite::TechnicalSpec.
See inst/upgrade/1.7.0/site_category.pl for all of the relevant SQL.
The site__id for existing categories has been set to the default site with ID 100.
Added a second category to be the root category for the default site. The original category will be kept as a master category. It has an ID of 0 and its uri field set to an empty string, ''. SQL for other classes refering to the old root category have been updated. To point at the new root category set up for the default site.
See inst/upgrade/1.7.0/site_category.pl for all of the relevant SQL.
The tests are all in Bric::Biz::Category::Test and Bric::Biz::Category::DevTest in the t directory.
The Bric::Biz::Category interface is completely documented in POD of this package.
The interface for Bric::Biz::OutputChannel follows the changes called for in MultiSite::TechnicalSpec.
The Bric::Biz::OutputChannel SQL follows the changes called for in MultiSite::TechnicalSpec.
See inst/upgrade/1.7.0/site_output_channel.pl for all of the relevant SQL.
The site__id for existing output channels has been set to the default site with ID 100.
The protocol field has been left null.
See inst/upgrade/1.7.0/site_output_channel.pl for all of the relevant SQL.
The tests are all in Bric::Biz::OutputChannel::Test and Bric::Biz::OutputChannel::DevTest in the t directory.
The Bric::Biz::OutputChannel interface is completely documented in POD of this package.
The interface for Bric::Biz::Asset follows the changes called for in MultiSite::TechnicalSpec regarding key_name.
The Asset::Business types have been changed to require a site_id for the
selected asset. A new attribute alias_id has been added so an asset
can be an alias to another asset so you can link assets between multiple
sites.
The Bric::Biz::Asset SQL follows the changes called for in MultiSite::TechnicalSpec regarding key_name.
See inst/upgrade/1.7.0/site_asset.pl for all of the relevant SQL.
A new column, site__id was added with a foreign key to the site table.
For the Story and Media type, a new column alias_id was added with a
foreign key to itself.
All current stories are transferred to the default site.
The interface for Bric::Biz::AssetType follows the changes called for in MultiSite::TechnicalSpec regarding key_name.
The interface now contains the following added functions
The following interfaces has been changed
get_primary_oc_id()set_primary_oc_id(id)A new collection has been added to manage the relationship between sites and elements. The class is named Bric::Util::Coll::Site and is used internally by the AssetType class.
The Bric::Biz::AssetType SQL follows the changes called for in MultiSite::TechnicalSpec regarding key_name.
See inst/upgrade/1.7.0/asset_type_key_name.pl for all of the relevant SQL.
For element to site mapping a new table was created called element__site as specified in MultiSite::TechnicalSpec, however the id/pik was not necessary to maintain integrity and the element__output_channel was not needed to be redone since it contains information about both the site__id and the element__id.
The upgrade script is inst/upgrate/1.7.0/site_element.pl.
The key_name field has been set to the name field after it has been lowercased and all characters that are not a-z converted into underscores ('_').
See inst/upgrade/1.7.0/asset_type_key_name.pl for all of the relevant SQL.
The selected output channels and primary_oc_id information is moved to the element__site table by the upgrade inst/upgrate/1.7.0/site_element.pl script. It is per default associated with the default site with an id of 100.
The tests are all in Bric::Biz::AssetType::Test and Bric::Biz::AssetType::DevTest in the t directory.
The Bric::Biz::AssetType interface is completely documented in POD of this package.
The interface for Bric::Biz::AssetType::Parts::Data follows the changes called for in MultiSite::TechnicalSpec.
The Bric::Biz::AssetType::Parts::Data SQL follows the changes called for in MultiSite::TechnicalSpec.
See inst/upgrade/1.7.0/asset_type_data_key_name.pl for all of the relevant SQL.
The 'name' field is renamed to 'key_name', its data has been lowercased and all characters that are not a-z converted to underscores ('_'). Data that would be the 'name' field exist already in the 'disp' key of the metadata for the attribute 'html_info'
See inst/upgrade/1.7.0/asset_type_data_key_name.pl for all of the relevant SQL.
The tests are all in Bric::Biz::AssetType::Parts::Data::Test and Bric::Biz::AssetType::Parts::Data::DevTest in the t directory.
The Bric::Biz::AssetType::Parts::Data interface is completely documented in POD of this package.
The interface for Bric::Biz::Asset::Business::Parts::Tile::Container follows the changes called for in MultiSite::TechnicalSpec.
The Bric::Biz::Asset::Business::Parts::Tile::Container SQL follows the changes called for in MultiSite::TechnicalSpec.
See inst/upgrade/1.7.0/container_tile_key_name.pl for all of the relevant SQL.
The name field is renamed to 'key_name', its data has been lowercased and all characters that are not a-z converted to underscores ('_').
See inst/upgrade/1.7.0/container_tile_key_name.pl for all of the relevant SQL.
The tests are all in Bric::Biz::Asset::Business::Parts::Tile::Container::Test and Bric::Biz::Asset::Business::Parts::Tile::Container::DevTest in the t directory.
The Bric::Biz::Asset::Business::Parts::Tile::Container interface is completely documented in POD of this package.
The interface for Bric::Biz::Asset::Business::Parts::Tile::Data follows the changes called for in MultiSite::TechnicalSpec.
The Bric::Biz::Asset::Business::Parts::Tile::Data SQL follows the changes called for in MultiSite::TechnicalSpec.
See inst/upgrade/1.7.0/data_tile_key_name.pl for all of the relevant SQL.
The name field is renamed to 'key_name', its data has been lowercased and all characters that are not a-z converted to underscores ('_').
See inst/upgrade/1.7.0/data_tile_key_name.pl for all of the relevant SQL.
The tests are all in Bric::Biz::Asset::Business::Parts::Tile::Data::Test and Bric::Biz::Asset::Business::Parts::Tile::Data::DevTest in the t directory.
The Bric::Biz::Asset::Business::Parts::Tile::Data interface is completely documented in POD of this package.
The interface for Bric::Dist::ServerType follows the changes called for in MultiSite::TechnicalSpec.
The Bric::Dist::ServerType SQL follows the changes called for in MultiSite::TechnicalSpec.
See inst/upgrade/1.7.0/site_server_type.pl for all of the relevant SQL updates.
The 'site__id' field has been addded to the server_type table and populated with ID 100, the default site.
See inst/upgrade/1.7.0/data_tile_key_name.pl for all of the relevant SQL.
The tests are all in Bric::Dist::ServerType::Test and Bric::Dist::ServerType::DevTest in the t directory.
The Bric::Dist::ServerType interface is completely documented in POD of this package.
A new instance attribute site_id was added, new set_site_id and
get_site_id methods where added.
Methods new, list and lookup get support for new parameter
site_id. A workflow is no longer unique only on it's name.
The Bric::Biz::Workflow SQL follows the changes called for in MultiSite::TechnicalSpec.
New column site__id added the workflow table, unique index changed to
be site__id + LOWER(name) but using a custom 'lower_text_num' function.
See inst/upgrade/1.7.0/workflow_site.pl for all of the relevant SQL.
The site__id for existing workflows has been set to the default site with ID 100.
Tests have been added to Bric::Biz::Workflow::DevTest in the t directory, for all new interfaces.
The Bric::Biz::Workflow interface is completely documented in POD of this package.
For those with access to more than one site, a new ``Site Context'' select list has been added to the top of every page. It was placed in all pages, rather than just on ``My Workspace'', because the ``Context'' bar just below the title tab seemed the most appropriate place for it. It's also a lot more convenient for users. The list is managed by a new widget, comp/widgets/site_context/site_context.mc, which has an associated callback to change the context of a user when a new one is selected. The currently-selected context for each user is cached by Bric::App::Cache so that it will persist across sessions.
A special ``All Sites'' option may be enabled by a new bricolage.conf
configuration directive, ALLOW_ALL_SITES_CX. When enabled, an additional
option, ``All Sites'', is added to the Site Context menu. When selected, this
option will allow the user to see the workflows for all sites she's associated
with; the ALLOW_ALL_SITES_CX directive is off by default.
The list of sites is cached by Bric::App::Cache, and the list of sites for a given user is stored in her session. Changes made to any site, user, user group, or site group object lead the list of sites in the cache to be expired and reloaded from the database. This approach optimizes the performance of the Site Context list while allowing changes to group permissions to affect all users immediately. The approach taken is similar to that historically used for displaying workflows in the side navigation bar.
The new site manager code was added to comp/admin/manager/dhandler. It
lists the name and domain_name properties of site objects, refers select
actions to comp/admin/profile/site/dhandler, and delete actions to
comp/widgets/site/callback.mc. The latter is a new callback, used instead
of listManager's default deactivate callback to ensure that the list of sites
in the cache gets expired and will be reloaded by the sideNav component on the
next request.
This brand new profile is designed to manage site objects. It's actually relatively straight-forward, and simpler than that described in the technical spec. It does, of course, have an ``Information'' section, in which its basic properties (Name, Description, Domain Name) can be edited. It also includes a doubleList Manger section so that a site can be associated with site groups. (Note that I've created a ToDo item for moving this functionality out of the User, Category, and Site Profiles and into a widget to handle group association for all major administration profiles).
The third section with the user group associations I've decided to leave out for now. The reason for this is that most of the permissions granted to access the assets in a site by users will be managed in the user profile, where the secret READ, EDIT, CREATE, and DENY groups for each site will be displayed and a user can be associated. While it might be convenient to also do some permissions assignment in the site profile itself, such is not crucial for getting this feature completed. Furthermore, this functionality would also be welcome in the element, category, and workflow profiles. It makes sense, then, to add this functionality to all of those profiles later as a widget. Thus, I've decided to leave it for later, when the site functionality is actually fully functional and we have time to add bells and whistles such as this.
The callback for the site profile is fairly simple, since it needs to handle
only a few properties of a site. Furthermore, since the Bric::Biz::Site class
handles data validation itself (such as ensuring that the Name and Domain Name
attributes are unique), the callback doesn't have to do any validation
itself. Rather, it merely takes care of the assignments inside an eval{}
block, and does the right thing with any exceptions it catches. If it catches
a Bric::Util::Fault::Error exception, it uses the $lang global to
localize the exception object's maketext attribute and passes the result
to add_msg(). It then returns the object, where the user will see her
localized error message and make any changes.
Otherwise, the callback continues to process the group associations (using code borrowed form the user profile callback), expires the list of sites in the cache, logs the appropriate event and then returns the user to the site manager.
Delete actions are handled a bit differently. A site to be deleted is deactivated and saved, the list of sites in the cache is cleared, an event is logged, and the user is returned to the site manager.
The group profile was modified so that by default it passes an active
attribute to class' list() methods. This is so that classes like
Bric::Biz::Site that need an explicit active parameter (or else return
active and inactive objects) will return only active objects.
The user profile was modified so that its call to
Bric::Util::Grp::User->list passes a true all parameter. This is so
that secret groups created by Bric::Biz::Site will be properly displayed. This
seems to be all right, as no other class appears to create secret user groups,
and none of the default user groups are secret.
David Wheeler <david@kineticode.com>