Invision Community 4.2 has introduced the new Clubs feature. This feature allows members to create their own sub-communities within the community. 3rd party developers can extend Clubs to allow users to add Nodes specific to that Club.
Implementing Club Support
In order to add Club support to your nodes, the following adjustments must be made to your Node model. First, you must add the \IPS\Content\ClubContainer trait to your Model, which will then expose the following methods from the trait. All of which can be overridden in your Model directly, if specific changes or checks need to be made.
/** * Get the database column which stores the club ID * * @return string */ public static function clubIdColumn() /** * Get front-end language string * * @return string */ public static function clubFrontTitle() /** * Get acp language string * * @return string */ public static function clubAcpTitle() /** * Get the associated club * * @return \IPS\Member\Club|NULL */ public function club() /** * Set form for creating a node of this type in a club * * @param \IPS\Helpers\Form $form Form object * @return void */ public function clubForm( \IPS\Helpers\Form $form ) /** * Save club form * * @param \IPS\Member\Club $club The club * @param array $values Values * @return void */ public function saveClubForm( \IPS\Member\Club $club, $values ) /** * Class-specific routine when saving club form * * @param \IPS\Member\Club $club The club * @param array $values Values * @return void */ public function _saveClubForm( \IPS\Member\Club $club, $values ) /** * Set the permission index permissions to a specific club * * @param \IPS\Member\Club $club The club * @return void */ public function setPermissionsToClub( \IPS\Member\Club $club ) /** * Check Moderator Permission * * @param string $type 'edit', 'hide', 'unhide', 'delete', etc. * @param \IPS\Member|NULL $member The member to check for or NULL for the currently logged in member * @return bool */ public function modPermission( $type, \IPS\Member $member ) /** * [Node] Get parent list * * @return \SplStack */ public function parents()
Some important notes about specific methods:
- clubIdColumn() - This will be the name of the column without the database prefix. So, if your column is node_club_id and your database prefix is node_, then this method should only return club_id. The default return value from the trait is club_id.
- clubForm() - By default, this method will simply add a Text field for the name of the Node within the club. You can optionally, however, overload this method to add more elements to the form (such as a description, for example). If your node only needs a title, then it is not necessary to overload this method.
- _saveClubForm() - By default, this method does nothing. If Nodes need to perform additional processing before the club node is saved, then they should overload this method, rather than saveClubForm().
- modPermission() - This method automatically overloads \IPS\Node\Model::modPermission() and will automatically check the permissions of both club specific moderators (as assigned by club leaders), club leaders, club owners,, as well as actual site moderators who are not already a part of the club.
- parents() - This method automatically overloads \IPS\Node\Mode::parents() and will always return an empty SplStack object if the node is associated with a club.
Interface
A few alterations to your templates, as well as your controllers, need to be made to fully implements the Clubs look and feel into your nodes. Club nodes will load from within the context of your Application, and thus share the same URL structure as normal nodes, and will also use the same controllers and templates.
For your controllers, you need to adjust your breadcrumb navigation to account for the fact that the node is a part of a club, rather than as a normal, standalone, node.
If your application uses \IPS\Helpers\Table\Content to display content from within a node, then this will automatically be taken care of for you. Otherwise, implementing proper navigation is as simple as:
if ( \IPS\IPS::classUsesTrait( $container, 'IPS\Content\ClubContainer' ) and $club = $container->club() ) { \IPS\core\FrontNavigation::$clubTabActive = TRUE; \IPS\Output::i()->breadcrumb = array(); \IPS\Output::i()->breadcrumb[] = array( \IPS\Http\Url::internal( 'app=core&module=clubs&controller=directory', 'front', 'clubs_list' ), \IPS\Member::loggedIn()->language()->addToStack('module__core_clubs') ); \IPS\Output::i()->breadcrumb[] = array( $club->url(), $club->name ); if ( \IPS\Settings::i()->clubs_header == 'sidebar' ) { \IPS\Output::i()->sidebar['contextual'] = \IPS\Theme::i()->getTemplate( 'clubs', 'core' )->header( $club, $container, 'sidebar' ); } } else { foreach ( $container->parents() as $parent ) { \IPS\Output::i()->breadcrumb[] = array( $parent->url(), $parent->_title ); } }
The club header / sidebar, however, is not handled automatically and must be manually inserted into your templates when viewing a node, or content item within a node, that belongs to a club. For example:
{{if $club = $node->club()}} {{if settings.clubs and settings.clubs_header == 'full'}} {template="header" app="core" group="clubs" params="$club, $node"} {{endif}} <div id='elClubContainer'> {{endif}} <1-- My Template Content Here --> {{if $node->club()}} </div> {{endif}}
If a node is associated with a club, then the inner content must be wrapped in a <div> element, with the id elClubContainer. The same also applies when viewing a content item from within a club node:
{{if $club = $item->container()->club()}} {{if settings.clubs and settings.clubs_header == 'full'}} {template="header" app="core" group="clubs" params="$club, $item->container()"} {{endif}} <div id='elClubContainer'> {{endif}} <!-- My Content Item Template Content Here --> {{if $item->container()->club()}} </div> {{endif}}
The Clubs
All Clubs are an ActiveRecord using the \IPS\Member\Club class.
namespace IPS\Member; /** * Club Model */ class _Club extends \IPS\Patterns\ActiveRecord implements \IPS\Content\Embeddable { const TYPE_PUBLIC = 'public'; const TYPE_OPEN = 'open'; const TYPE_CLOSED = 'closed'; const TYPE_PRIVATE = 'private'; const STATUS_MEMBER = 'member'; const STATUS_INVITED = 'invited'; const STATUS_REQUESTED = 'requested'; const STATUS_DECLINED = 'declined'; const STATUS_BANNED = 'banned'; const STATUS_MODERATOR = 'moderator'; const STATUS_LEADER = 'leader'; /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'core_clubs'; /** * Construct ActiveRecord from database row * * @param array $data Row from database table * @param bool $updateMultitonStoreIfExists Replace current object in multiton store if it already exists there? * @return static */ public static function constructFromData( $data, $updateMultitonStoreIfExists = TRUE ) /** * Get all clubs a member can see * * @param \IPS\Member $member The member to base permission off or NULL for all clubs * @param int $limit Number to get * @param string $sortOption The sort option ('last_activity', 'members', 'content' or 'created') * @param bool|\IPS\Member $mineOnly Limit to clubs a particular member has joined (TRUE to use the same value as $member) * @param array $filters Custom field filters * @param mixed $extraWhere Additional WHERE clause * @return \IPS\Patterns\ActiveRecordIterator */ public static function clubs( \IPS\Member $member = NULL, $limit, $sortOption, $mineOnly=FALSE, $filters=array(), $extraWhere=NULL ) /** * Get number clubs a member is leader of * * @param \IPS\Member $member The member * @return int */ public static function numberOfClubsMemberIsLeaderOf( \IPS\Member $member ) /* !ActiveRecord */ /** * Set Default Values * * @return void */ public function setDefaultValues() /** * Get owner * * @return \IPS\Member|NULL */ public function get_owner() /** * Set member * * @param \IPS\Member * @return void */ public function set_owner( \IPS\Member $owner = NULL ) /** * Get created date * * @return \IPS\DateTime */ public function get_created() /** * Set created date * * @param \IPS\DateTime $date The invoice date * @return void */ public function set_created( \IPS\DateTime $date ) /** * Get club URL * * @return \IPS\Http\Url */ public function url() /** * Columns needed to query for search result / stream view * * @return array */ public static function basicDataColumns() /** * Edit Club Form * * @param bool $acp TRUE if editing in the ACP * @param bool $new TRUE if creating new * @param array $availableTypes If creating new, the available types * @return \IPS\Helpers\Form|NULL */ public function form( $acp=FALSE, $new=FALSE, $availableTypes=NULL ) /** * Custom Field Values * * @return array */ public function fieldValues() /** * Cover Photo * * @param bool $getOverlay If FALSE, will not set the overlay, which saves queries if it will not be used (such as in clubCard) * @return \IPS\Helpers\CoverPhoto */ public function coverPhoto( $getOverlay=TRUE, $position='full' ) /** * Location * * @return \IPS\GeoLocation|NULL */ public function location() /** * Get members * * @param array $statuses The membership statuses to get * @param int $limit Number to get * @param string $order ORDER BY clause * @param int $returnType 0 = core_clubs_memberships rows, 1 = core_clubs_memberships plus \IPS\Member::columnsForPhoto(), 2 = full core_members rows, 3 = same as 1 but also getting name of adder/invitee, 4 = count only * @return \IPS\Db\Select|int */ public function members( $statuses = array( 'member', 'moderator', 'leader' ), $limit = 25, $order = 'core_clubs_memberships.joined ASC', $returnType = 1 ) /** * Get basic data of a random ten members in the club (for cards) * * @return array */ public function randomTenMembers() /** * Add a member * * @param \IPS\Member $member The member * @param bool $status Status * @param bool $update Update membership if already a member? * @param \IPS\Member|NULL $addedBy The leader who added them, or NULL if joining themselves * @param \IPS\Member|NULL $invitedBy The member who invited them, or NULL if joining themselves * @return void * @throws \OverflowException Member is already in the club and $update was FALSE */ public function addMember( \IPS\Member $member, $status = 'member', $update = FALSE, \IPS\Member $addedBy = NULL, \IPS\Member $invitedBy = NULL ) /** * Remove a member * * @param \IPS\Member $member The member * @return void */ public function removeMember( \IPS\Member $member ) /** * Recount members * * @return void */ public function recountMembers() /* !Manage Nodes */ /** * Get available features * * @param \IPS\Member|NULL If a member object is provided, will opnly get the types that member can create * @return array */ public static function availableNodeTypes( \IPS\Member $member = NULL ) /** * Get Node names and URLs * * @return array */ public function nodes() /** * Can a member see this club and who's in it? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canView( \IPS\Member $member = NULL ) /** * Can a member join (or ask to join) this club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canJoin( \IPS\Member $member = NULL ) /** * Can a member see the posts in this club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canRead( \IPS\Member $member = NULL ) /** * Can a member participate this club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canPost( \IPS\Member $member = NULL ) /** * Can a member invite other members * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function canInvite( \IPS\Member $member = NULL ) /** * Does this user have leader permissions in the club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function isLeader( \IPS\Member $member = NULL ) /** * Does this user have moderator permissions in the club? * * @param \IPS\Member $member The member (NULL for currently logged in member) * @return bool */ public function isModerator( \IPS\Member $member = NULL ) /** * Get status of a particular member * * @param \IPS\Member $member The member * @param int $returnType 1 will return a string with the type or NULL if not applicable. 2 will return array with status, joined, accepted_by, invited_by * @return mixed */ public function memberStatus( \IPS\Member $member, $returnType = 1 ) }
Custom Fields
Clubs can also have Custom Fields defined. These are on a per club basis, rather than per Node. If your Node implements Custom Fields per node as a feature, then you will need to ensure those custom fields are present within your Node models clubForm() method.
Aside from the methods defined in the base \IPS\CustomField class, the following methods are defined within the \IPS\Member\Club\CustomField class.
namespace IPS\Member\Club; /** * Clubs Customer Field Node */ class _CustomField extends \IPS\CustomField { /** * @brief [ActiveRecord] Multiton Store */ protected static $multitons; /** * @brief [ActiveRecord] Database Table */ public static $databaseTable = 'core_clubs_fields'; /** * @brief [ActiveRecord] Database Prefix */ public static $databasePrefix = 'f_'; /** * @brief [Node] Order Database Column */ public static $databaseColumnOrder = 'position'; /** * @brief [CustomField] Title/Description lang prefix */ protected static $langKey = 'core_clubfield'; /** * @brief [CustomField] Content database table */ protected static $contentDatabaseTable = 'core_clubs_fieldvalues'; /** * @brief [Node] ACP Restrictions * @code array( 'app' => 'core', // The application key which holds the restrictrions 'module' => 'foo', // The module key which holds the restrictions 'map' => array( // [Optional] The key for each restriction - can alternatively use "prefix" 'add' => 'foo_add', 'edit' => 'foo_edit', 'permissions' => 'foo_perms', 'delete' => 'foo_delete' ), 'all' => 'foo_manage', // [Optional] The key to use for any restriction not provided in the map (only needed if not providing all 4) 'prefix' => 'foo_', // [Optional] Rather than specifying each key in the map, you can specify a prefix, and it will automatically look for restrictions with the key "[prefix]_add/edit/permissions/delete" * @endcode */ protected static $restrictions = array( 'app' => 'core', 'module' => 'clubs', 'all' => 'fields_manage' ); /** * @brief [Node] Node Title */ public static $nodeTitle = 'clubs_custom_fields'; /** * @brief [Node] Title prefix. If specified, will look for a language key with "{$key}_title" as the key */ public static $titleLangPrefix = 'core_clubfield_'; /** * @brief [CustomField] Column Map */ public static $databaseColumnMap = array( 'content' => 'extra', 'not_null' => 'required', ); /** * @brief [CustomField] Additional Field Toggles */ public static $additionalFieldToggles = array( 'Checkbox' => array( 'f_filterable' ), 'CheckboxSet' => array( 'f_filterable' ), 'Radio' => array( 'f_filterable' ), 'Select' => array( 'f_filterable' ), 'YesNo' => array( 'f_filterable' ), ); /** * @brief [CustomField] Editor Options */ public static $editorOptions = array( 'app' => 'core', 'key' => 'Clubs' ); /** * @brief [CustomField] Upload Storage Extension */ public static $uploadStorageExtension = 'core_'; /** * Get fields * * @return array */ public static function fields() /** * Get if there are any filterable fields * * @return bool */ public static function areFilterableFields() /** * [Node] Add/Edit Form * * @param \IPS\Helpers\Form $form The form * @return void */ public function form( &$form ) /** * [ActiveRecord] Save Record * * @return void */ public function save() /** * [ActiveRecord] Delete Record * * @return void */ public function delete() /** * [Node] Format form values from add/edit form for save * * @param array $values Values from the form * @return array */ public function formatFormValues( $values ) }
Report Document