<html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"><title>Groups Design</title><meta name="generator" content="DocBook XSL Stylesheets V1.65.1"><link rel="home" href="index.html" title="OpenACS Core Documentation"><link rel="up" href="kernel-doc.html" title="Chapter�17.�Kernel Documentation"><link rel="previous" href="groups-requirements.html" title="Groups Requirements"><link rel="next" href="subsites-requirements.html" title="Subsites Requirements"><link rel="stylesheet" href="openacs.css" type="text/css"></head><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><div class="navheader"><a href="http://openacs.org"><img src="/doc/images/alex.jpg" border="0" alt="Alex logo"></a><table width="100%" summary="Navigation header" border="0"><tr><td width="20%" align="left"><a accesskey="p" href="groups-requirements.html">Prev</a> </td><th width="60%" align="center">Chapter�17.�Kernel Documentation</th><td width="20%" align="right"> <a accesskey="n" href="subsites-requirements.html">Next</a></td></tr></table><hr></div><div class="sect1" lang="en"><div class="titlepage"><div><div><h2 class="title" style="clear: both"><a name="groups-design"></a>Groups Design</h2></div></div><div></div></div><div class="authorblurb"><p>By <a href="http://planitia.org" target="_top">Rafael H. Schloming</a> and Mark Thomas</p> OpenACS docs are written by the named authors, and may be edited by OpenACS documentation staff. </div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-essentials"></a>Essentials</h3></div></div><div></div></div><div class="itemizedlist"><ul type="disc"><li><p>User directory</p></li><li><p>Sitewide administrator directory</p></li><li><p>Subsite administrator directory</p></li><li><p>TCL script directory</p></li><li><p><a href="groups-requirements.html">OpenACS 4 Groups Requirements</a></p></li><li><p>Data model</p></li><li><p>PL/SQL file </p><div class="itemizedlist"><ul type="circle"><li><p><a href="/doc/sql/display-sql?url=community-core-create.sql&package_key=acs-kernel" target="_top"> community-core-create.sql</a></p></li><li><p><a href="/doc/sql/display-sql?url=groups-create.sql&package_key=acs-kernel" target="_top">groups-create.sql</a></p></li></ul></div></li><li><p>ER diagram</p></li><li><p>Transaction flow diagram</p></li></ul></div></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-intro"></a>Introduction</h3></div></div><div></div></div><p>Almost all database-backed websites have users, and need to model the grouping of users. The OpenACS 4 Parties and Groups system is intended to provide the flexibility needed to model complex real-world organizational structures, particularly to support powerful subsite services; that is, where one OpenACS installation can support what appears to the user as distinct web services for different user communities.</p></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-hist-considerations"></a>Historical Considerations</h3></div></div><div></div></div><p>The primary limitation of the OpenACS 3.x user group system is that it restricts the application developer to representing a "flat group" that contains only users: The <tt class="computeroutput">user_groups</tt> table may contain the <tt class="computeroutput">group_id</tt> of a parent group, but parent-child relationship support is limited because it only allows one kind of relationship between groups to be represented. Moreover, the Oracle database's limited support for tree-like structures makes the queries over these relationships expensive.</p><p>In addition, the Module Scoping design in OpenACS 3.0 introduced a <span class="emphasis"><em>party</em></span> abstraction - a thing that is a person or a group of people - though not in the form of an explicit table. Rather, the triple of <tt class="computeroutput">scope</tt>, <tt class="computeroutput">user_id</tt>, and <tt class="computeroutput">group_id</tt> columns was used to identify the party. One disadvantage of this design convention is that it increases a data model's complexity by requiring the programmer to:</p><div class="itemizedlist"><ul type="disc"><li><p>add these three columns to each "scoped" table</p></li><li><p>define a multi-column check constraint to protect against data corruption (e.g., a row with a <tt class="computeroutput">scope</tt> value of "group" but a null <tt class="computeroutput">group_id</tt>)</p></li><li><p>perform extra checks in <tt class="computeroutput">Tcl</tt> and <tt class="computeroutput">PL/SQL</tt> functions and procedures to check both the <tt class="computeroutput">user_id</tt> and <tt class="computeroutput">group_id</tt> values</p></li></ul></div></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-competitors"></a>Competitive Analysis</h3></div></div><div></div></div><p>...</p></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-design-tradeoffs"></a>Design Tradeoffs</h3></div></div><div></div></div><p>The core of the Group Systems data model is quite simple, but it was designed in the hopes of modeling "real world" organizations which can be complex graph structures. The Groups System only considers groups that can be modeled using directed acyclic graphs, but queries over these structures are still complex enough to slow the system down. Since almost every page will have at least one membership check, a number of triggers, views, and auxiliary tables have been created in the hopes of increasing performance. To keep the triggers simple and the number of triggers small, the data model disallows updates on the membership and composition tables, only inserts and deletes are permitted.</p><p>The data model has tried to balance the need to model actual organizations without making the system too complex or too slow. The added triggers, views, and tables and will increase storage requirements and the insert and delete times in an effort to speed access time. The limited flexibility (no updates on membership) trades against the complexity of the code.</p></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-data-model"></a>Data Model Discussion</h3></div></div><div></div></div><p>The Group System data model consists of the following tables:</p><div class="variablelist"><dl><dt><span class="term"><tt class="computeroutput">parties</tt> </span></dt><dd><p>The set of all defined parties: any <span class="emphasis"><em>person</em></span>, <span class="emphasis"><em>user</em></span>, or <span class="emphasis"><em>group</em></span> must have a corresponding row in this table.</p></dd><dt><span class="term"><tt class="computeroutput">persons</tt> </span></dt><dd><p>The set of all defined persons. To allow easy sorting of persons, the name requirement <a href="groups-requirements.html#groups-requirements-30-10">30.10</a> is met by splitting the person's name into two columns: <tt class="computeroutput">first_names</tt> and <tt class="computeroutput">last_name</tt>.</p></dd><dt><span class="term"><tt class="computeroutput">users</tt> </span></dt><dd><p>The set of all registered users; this table includes information about the user's email address and the user's visits to the site.</p></dd><dt><span class="term"><tt class="computeroutput">user_preferences</tt> </span></dt><dd><p>Preferences for the user.</p></dd><dt><span class="term"><tt class="computeroutput">groups</tt> </span></dt><dd><p>The set of all defined groups.</p></dd><dt><span class="term"><tt class="computeroutput">group_types</tt> </span></dt><dd><p>When a new type of group is created, this table holds additional knowledge level attributes for the group and its subtypes.</p></dd><dt><span class="term"><tt class="computeroutput">membership_rels</tt> </span></dt><dd><p>The set of direct membership relationships between a group and a party.</p></dd><dt><span class="term"><tt class="computeroutput">group_member_index</tt> </span></dt><dd><p>A mapping of a party <span class="strong">P</span> to the groups {<span class="strong">G<sub>i</sub></span>}the party is a member of; this mapping includes the type of relationship by including the appropriate<tt class="computeroutput">rel_id</tt> from the <tt class="computeroutput">membership_rels</tt> table.</p></dd><dt><span class="term"><tt class="computeroutput">composition_rels</tt> </span></dt><dd><p>The set of direct component relationships between a group and another group.</p></dd><dt><span class="term"><tt class="computeroutput">group_component_index</tt> </span></dt><dd><p>A mapping of a group <span class="strong">G</span>to the set of groups {<span class="strong">G<sub>i</sub></span>} that <span class="strong">G</span> is a component of; this mapping includes the type of relationship by including the appropriate<tt class="computeroutput">rel_id</tt> from the <tt class="computeroutput">composition_rels</tt> table.</p></dd></dl></div><p>New groups are created through the <tt class="computeroutput">group.new</tt> constructor. When a specialized type of group is required, the group type can be extended by an application developer. Membership constraints can be specified at creation time by passing a parent group to the constructor.</p><p>The <tt class="computeroutput">membership_rels</tt> and <tt class="computeroutput">composition_rels</tt> tables indicate a group's direct members and direct components; these tables do not provide a record of the members or components that are in the group by virtue of being a member or component of one of the group's component groups. Site pages will query group membership often, but the network of component groups can become a very complex directed acyclic graph and traversing this graph for every query will quickly degrade performance. To make membership queries responsive, the data model includes triggers (described in the next paragraph) which watch for changes in membership or composition and update tables that maintain the group party mappings, i.e., <tt class="computeroutput">group_member_index</tt> and <tt class="computeroutput">group_component_index</tt>. One can think of these tables as a manually maintained index.</p><p>The following triggers keep the <tt class="computeroutput">group_*_index</tt> tables up to date:</p><div class="variablelist"><dl><dt><span class="term"><tt class="computeroutput">membership_rels_in_tr</tt> </span></dt><dd><p>Is executed when a new group/member relationship is created (an insert on <tt class="computeroutput">membership_rels</tt>)</p></dd><dt><span class="term"><tt class="computeroutput">membership_rels_del_tr</tt> </span></dt><dd><p>Is executed when a group/member relationship is deleted (a delete on <tt class="computeroutput">membership_rels</tt>)</p></dd><dt><span class="term"><tt class="computeroutput">composition_rels_in_tr</tt> </span></dt><dd><p>Is executed when a new group/component relationship is created (an insert on <tt class="computeroutput">composition_rels</tt>)</p></dd><dt><span class="term"><tt class="computeroutput">composition_rels_del_tr</tt> </span></dt><dd><p>Is executed when a group/component relationship is deleted (a delete on <tt class="computeroutput">composition_rels</tt>)</p></dd></dl></div><p>The data model provides the following views onto the <tt class="computeroutput">group_member_index</tt> and <tt class="computeroutput">group_component_index</tt> tables. No code outside of Groups System should modify the <tt class="computeroutput">group_*_index</tt> tables.</p><div class="variablelist"><dl><dt><span class="term"><tt class="computeroutput">group_member_map</tt> </span></dt><dd><p>A mapping of a party to the groups the party is a member of; this mapping includes the type of relationship by including the appropriate<tt class="computeroutput">rel_id</tt> from the <tt class="computeroutput">membership_rels</tt> table.</p></dd><dt><span class="term"><tt class="computeroutput">group_approved_member_map</tt> </span></dt><dd><p>A mapping of a party to the groups the party is an approved member of (<tt class="computeroutput">member_state</tt> is 'approved'); this mapping includes the type of relationship by including the appropriate<tt class="computeroutput">rel_id</tt> from the <tt class="computeroutput">membership_rels</tt> table.</p></dd><dt><span class="term"><tt class="computeroutput">group_distinct_member_map</tt> </span></dt><dd><p>A person may appear in the group member map multiple times, for example, by being a member of two different groups that are both components of a third group. This view is strictly a mapping of <span class="strong">approved</span> members to groups.</p></dd><dt><span class="term"><tt class="computeroutput">group_component_map</tt> </span></dt><dd><p>A mapping of a group <span class="strong">G</span>to the set of groups {<span class="strong">G<sub>i</sub></span>} group <span class="strong">G</span> is a component of; this mapping includes the type of relationship by including the appropriate<tt class="computeroutput">rel_id</tt> from the <tt class="computeroutput">composition_rels</tt> table.</p></dd><dt><span class="term"><tt class="computeroutput">party_member_map</tt> </span></dt><dd><p>A mapping of a party <span class="strong">P</span> to the set of parties {<span class="strong">P<sub>i</sub></span>} party <span class="strong">P</span> is a member of.</p></dd><dt><span class="term"><tt class="computeroutput">party_approved_member_map</tt> </span></dt><dd><p>A mapping of a party <span class="strong">P</span> to the set of parties {<span class="strong">P<sub>i</sub></span>} party <span class="strong">P</span> is an <span class="strong">approved</span> member of.</p></dd></dl></div></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-api"></a>API</h3></div></div><div></div></div><p> The API consists of tables and views and PL/SQL functions. </p><div class="sect3" lang="en"><div class="titlepage"><div><div><h4 class="title"><a name="groups-design-tables-views"></a>Tables and Views</h4></div></div><div></div></div><p>The <tt class="computeroutput">group_types</tt> table is used to create new types of groups.</p><p>The <tt class="computeroutput">group_member_map</tt>, <tt class="computeroutput">group_approved_member_map</tt>, <tt class="computeroutput">group_distinct_member_map</tt>, <tt class="computeroutput">group_component_map</tt>, <tt class="computeroutput">party_member_map</tt>, and <tt class="computeroutput">party_approved_member_map</tt> views are used to query group membership and composition.</p></div><div class="sect3" lang="en"><div class="titlepage"><div><div><h4 class="title"><a name="groups-design-pl-sql-api"></a>PL/SQL API</h4></div></div><div></div></div><p><span class="strong">Person</span></p><p><tt class="computeroutput">person.new</tt> creates a new person and returns the <tt class="computeroutput">person_id</tt>. The function must be given the full name of the person in two pieces: <tt class="computeroutput">first_names</tt> and <tt class="computeroutput">last_name</tt>. All other fields are optional and default to null except for <tt class="computeroutput">object_type</tt> which defaults to person and <tt class="computeroutput">creation_date</tt> which defaults to <tt class="computeroutput">sysdate</tt>. The interface for this function is:</p><pre class="programlisting"> function person.new ( person_id persons.person_id%TYPE, object_type acs_objects.object_type%TYPE, creation_date acs_objects.creation_date%TYPE, creation_user acs_objects.creation_user%TYPE, creation_ip acs_objects.creation_ip%TYPE, email parties.email%TYPE, url parties.url%TYPE, first_names persons.first_names%TYPE, last_name persons.last_name%TYPE ) return persons.person_id%TYPE; </pre><p><tt class="computeroutput">person.delete</tt> deletes the person whose <tt class="computeroutput">person_id</tt> is passed to it. The interface for this procedure is:</p><pre class="programlisting"> procedure person.delete ( person_id persons.person_id%TYPE ); </pre><p><tt class="computeroutput">person.name</tt> returns the name of the person whose <tt class="computeroutput">person_id</tt> is passed to it. The interface for this function is:</p><pre class="programlisting"> function person.name ( person_id persons.person_id%TYPE ) return varchar; </pre><p><span class="strong">User</span></p><p><tt class="computeroutput">acs_user.new</tt> creates a new user and returns the <tt class="computeroutput">user_id</tt>. The function must be given the user's email address and the full name of the user in two pieces: <tt class="computeroutput">first_names</tt> and <tt class="computeroutput">last_name</tt>. All other fields are optional. The interface for this function is:</p><pre class="programlisting"> function acs_user.new ( user_id users.user_id%TYPE, object_type acs_objects.object_type%TYPE, creation_date acs_objects.creation_date%TYPE, creation_user acs_objects.creation_user%TYPE, creation_ip acs_objects.creation_ip%TYPE, email parties.email%TYPE, url parties.url%TYPE, first_names persons.first_names%TYPE, last_name persons.last_name%TYPE password users.password%TYPE, salt users.salt%TYPE, password_question users.password_question%TYPE, password_answer users.password_answer%TYPE, screen_name users.screen_name%TYPE, email_verified_p users.email_verified_p%TYPE ) return users.user_id%TYPE; </pre><p><tt class="computeroutput">acs_user.delete</tt> deletes the user whose <tt class="computeroutput">user_id</tt> is passed to it. The interface for this procedure is:</p><pre class="programlisting"> procedure acs_user.delete ( user_id users.user_id%TYPE ); </pre><p><tt class="computeroutput">acs_user.receives_alerts_p</tt> returns 't' if the user should receive email alerts and 'f' otherwise. The interface for this function is:</p><pre class="programlisting"> function acs_user.receives_alerts_p ( user_id users.user_id%TYPE ) return varchar; </pre><p>Use the procedures <tt class="computeroutput">acs_user.approve_email</tt> and <tt class="computeroutput">acs_user.unapprove_email</tt> to specify whether the user's email address is valid. The interface for these procedures are:</p><pre class="programlisting"> procedure acs_user.approve_email ( user_id users.user_id%TYPE ); procedure acs_user.unapprove_email ( user_id users.user_id%TYPE ); </pre><p><span class="strong">Group</span></p><p><tt class="computeroutput">acs_group.new</tt> creates a new group and returns the <tt class="computeroutput">group_id</tt>. All fields are optional and default to null except for <tt class="computeroutput">object_type</tt> which defaults to 'group', <tt class="computeroutput">creation_date</tt> which defaults to <tt class="computeroutput">sysdate</tt>, and <tt class="computeroutput">group_name</tt> which is required. The interface for this function is:</p><pre class="programlisting"> function acs_group.new ( group_id groups.group_id%TYPE, object_type acs_objects.object_type%TYPE, creation_date acs_objects.creation_date%TYPE, creation_user acs_objects.creation_user%TYPE, creation_ip acs_objects.creation_ip%TYPE, email parties.email%TYPE, url parties.url%TYPE, group_name groups.group_name%TYPE ) return groups.group_id%TYPE; </pre><p><tt class="computeroutput">acs_group.name</tt> returns the name of the group whose <tt class="computeroutput">group_id</tt> is passed to it. The interface for this function is:</p><pre class="programlisting"> function acs_group.name ( group_id groups.group_id%TYPE ) return varchar; </pre><p><tt class="computeroutput">acs_group.member_p</tt> returns 't' if the specified party is a member of the specified group. Returns 'f' otherwise. The interface for this function is:</p><pre class="programlisting"> function acs_group.member_p ( group_id groups.group_id%TYPE, party_id parties.party_id%TYPE, ) return char; </pre><p><span class="strong">Membership Relationship</span></p><p><tt class="computeroutput">membership_rel.new</tt> creates a new membership relationship type between two parties and returns the relationship type's <tt class="computeroutput">rel_id</tt>. All fields are optional and default to null except for <tt class="computeroutput">rel_type</tt> which defaults to membership_rel. The interface for this function is:</p><pre class="programlisting"> function membership_rel.new ( rel_id membership_rels.rel_id%TYPE, rel_type acs_rels.rel_type%TYPE, object_id_one acs_rels.object_id_one%TYPE, object_id_two acs_rels.object_id_two%TYPE, member_state membership_rels.member_state%TYPE, creation_user acs_objects.creation_user%TYPE, creation_ip acs_objects.creation_ip%TYPE, ) return membership_rels.rel_id%TYPE; </pre><p><tt class="computeroutput">membership_rel.ban</tt> sets the <tt class="computeroutput">member_state</tt> of the given <tt class="computeroutput">rel_id</tt> to 'banned'. The interface for this procedure is:</p><pre class="programlisting"> procedure membership_rel.ban ( rel_id membership_rels.rel_id%TYPE ); </pre><p><tt class="computeroutput">membership_rel.approve</tt> sets the <tt class="computeroutput">member_state</tt> of the given <tt class="computeroutput">rel_id</tt> to 'approved'. The interface for this procedure is:</p><pre class="programlisting"> procedure membership_rel.approve ( rel_id membership_rels.rel_id%TYPE ); </pre><p><tt class="computeroutput">membership_rel.reject</tt> sets the <tt class="computeroutput">member_state</tt> of the given <tt class="computeroutput">rel_id</tt> to 'rejected. The interface for this procedure is:</p><pre class="programlisting"> procedure membership_rel.reject ( rel_id membership_rels.rel_id%TYPE ); </pre><p><tt class="computeroutput">membership_rel.unapprove</tt> sets the <tt class="computeroutput">member_state</tt> of the given <tt class="computeroutput">rel_id</tt> to an empty string ''. The interface for this procedure is:</p><pre class="programlisting"> procedure membership_rel.unapprove ( rel_id membership_rels.rel_id%TYPE ); </pre><p><tt class="computeroutput">membership_rel.deleted</tt> sets the <tt class="computeroutput">member_state</tt> of the given <tt class="computeroutput">rel_id</tt> to 'deleted'. The interface for this procedure is:</p><pre class="programlisting"> procedure membership_rel.deleted ( rel_id membership_rels.rel_id%TYPE ); </pre><p><tt class="computeroutput">membership_rel.delete</tt> deletes the given <tt class="computeroutput">rel_id</tt>. The interface for this procedure is:</p><pre class="programlisting"> procedure membership_rel.delete ( rel_id membership_rels.rel_id%TYPE ); </pre><p><span class="strong">Composition Relationship</span></p><p><tt class="computeroutput">composition_rel.new</tt> creates a new composition relationship type and returns the relationship's <tt class="computeroutput">rel_id</tt>. All fields are optional and default to null except for <tt class="computeroutput">rel_type</tt> which defaults to composition_rel. The interface for this function is:</p><pre class="programlisting"> function membership_rel.new ( rel_id composition_rels.rel_id%TYPE, rel_type acs_rels.rel_type%TYPE, object_id_one acs_rels.object_id_one%TYPE, object_id_two acs_rels.object_id_two%TYPE, creation_user acs_objects.creation_user%TYPE, creation_ip acs_objects.creation_ip%TYPE, ) return composition_rels.rel_id%TYPE; </pre><p><tt class="computeroutput">composition_rel.delete</tt> deletes the given <tt class="computeroutput">rel_id</tt>. The interface for this procedure is:</p><pre class="programlisting"> procedure membership_rel.delete ( rel_id composition_rels.rel_id%TYPE ); </pre></div></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-ui"></a>User Interface</h3></div></div><div></div></div><p>Describe the admin pages.</p></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-config"></a>Configuration/Parameters</h3></div></div><div></div></div><p>...</p></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-acc-tests"></a>Acceptance Tests</h3></div></div><div></div></div><p>...</p></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-future"></a>Future Improvements/Areas of Likely Change</h3></div></div><div></div></div><p>...</p></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-authors"></a>Authors</h3></div></div><div></div></div><div class="variablelist"><dl><dt><span class="term">System creator </span></dt><dd><p><a href="mailto:rhs@mit.edu" target="_top">Rafael H. Schloming</a></p></dd><dt><span class="term">System owner </span></dt><dd><p><a href="mailto:rhs@mit.edu" target="_top">Rafael H. Schloming</a></p></dd><dt><span class="term">Documentation author </span></dt><dd><p>Mark Thomas</p></dd></dl></div></div><div class="sect2" lang="en"><div class="titlepage"><div><div><h3 class="title"><a name="groups-design-rev-history"></a>Revision History</h3></div></div><div></div></div><div class="informaltable"><table cellspacing="0" border="1"><colgroup><col><col><col><col></colgroup><thead><tr><th><span class="strong">Document Revision #</span></th><th><span class="strong">Action Taken, Notes</span></th><th><span class="strong">When?</span></th><th><span class="strong">By Whom?</span></th></tr></thead><tbody><tr><td>0.1</td><td>Creation</td><td>08/22/2000</td><td><a href="mailto:rhs@mit.edu" target="_top">Rafael H. Schloming</a></td></tr><tr><td>0.2</td><td>Initial Revision</td><td>08/30/2000</td><td> Mark Thomas </td></tr><tr><td>0.3</td><td>Additional revisions; tried to clarify membership/compostion</td><td>09/08/2000</td><td> Mark Thomas </td></tr></tbody></table></div></div></div><div class="navfooter"><hr><table width="100%" summary="Navigation footer"><tr><td width="40%" align="left"><a accesskey="p" href="groups-requirements.html">Prev</a> </td><td width="20%" align="center"><a accesskey="h" href="index.html">Home</a></td><td width="40%" align="right"> <a accesskey="n" href="subsites-requirements.html">Next</a></td></tr><tr><td width="40%" align="left">Groups Requirements </td><td width="20%" align="center"><a accesskey="u" href="kernel-doc.html">Up</a></td><td width="40%" align="right"> Subsites Requirements</td></tr></table><hr><address><a href="mailto:docs@openacs.org">docs@openacs.org</a></address></div><a name="comments"></a><center><a href="http://openacs.org/doc/current/groups-design.html#comments">View comments on this page at openacs.org</a></center></body></html>