1
0
Fork 0
mirror of https://github.com/Oreolek/ifhub.club.git synced 2024-06-26 11:40:48 +03:00

Каркас системы инвайтов пользователей в закрытые блоги.

This commit is contained in:
Alexey Kachayev 2009-09-21 21:08:00 +00:00
parent f4ac7c67e8
commit 91ea2b8c8b
9 changed files with 406 additions and 28 deletions

View file

@ -95,7 +95,7 @@ class ActionBlog extends Action {
/**
* Достаём текущего пользователя
*/
$this->oUserCurrent=$this->User_GetUserCurrent();
$this->oUserCurrent=$this->User_GetUserCurrent();
/**
* Определяем какие блоки нужно выводить справа
*/
@ -122,8 +122,10 @@ class ActionBlog extends Action {
$this->AddEvent('add','EventAddBlog');
$this->AddEvent('edit','EventEditBlog');
$this->AddEvent('admin','EventAdminBlog');
$this->AddEvent('invite','EventInviteBlog');
$this->AddEvent('ajaxaddcomment','AjaxAddComment');
$this->AddEvent('ajaxaddbloginvite', 'AjaxAddBlogInvite');
$this->AddEventPreg('/^(\d+)\.html$/i','/^$/i','EventShowTopic');
$this->AddEventPreg('/^[\w\-\_]+$/i','/^(\d+)\.html$/i','EventShowTopic');
@ -394,7 +396,7 @@ class ActionBlog extends Action {
* Получаем список подписчиков блога
*/
$aBlogUsers=$this->Blog_GetBlogUsersByBlogId($oBlog->getId());
$this->Viewer_AddHtmlTitle($oBlog->getTitle());
$this->Viewer_AddHtmlTitle($this->Lang_Get('blog_admin'));
@ -405,8 +407,15 @@ class ActionBlog extends Action {
*/
$this->SetTemplateAction('admin');
/**
* Если блог закрытый, получаем приглашенных
* и добавляем блок-форму для приглашения
*/
if($oBlog->getType()=='close') {
$aBlogUsersInvited=$this->Blog_GetBlogUsersByBlogId($oBlog->getId(),LsBlog::BLOG_USER_ROLE_INVITE);
$this->Viewer_Assign('aBlogUsersInvited',$aBlogUsersInvited);
$this->Viewer_AddBlock('right','actions/ActionBlog/invited.tpl');
}
}
/**
@ -881,6 +890,265 @@ class ActionBlog extends Action {
$this->Message_AddErrorSingle($this->Lang_Get('system_error'),$this->Lang_Get('error'));
}
}
/**
* Обработка ajax запроса на отправку
* пользователям приглашения вступить в закрытый блог
*/
protected function AjaxAddBlogInvite() {
$this->Viewer_SetResponseAjax();
$sUsers=getRequest('users');
$sBlogId=getRequest('idBlog');
/**
* Если пользователь не авторизирован, возвращаем ошибку
*/
if (!$this->User_IsAuthorization()) {
$this->Message_AddErrorSingle($this->Lang_Get('need_authorization'),$this->Lang_Get('error'));
return;
}
$this->oUserCurrent=$this->User_GetUserCurrent();
/**
* Проверяем существование блога
*/
if(!$oBlog=$this->Blog_GetBlogById($sBlogId)) {
$this->Message_AddErrorSingle($this->Lang_Get('system_error'),$this->Lang_Get('error'));
return;
}
/**
* Проверяем, имеет ли право текущий пользователь добавлять invite в blog
*/
$oBlogUser=$this->Blog_GetBlogUserByBlogIdAndUserId($oBlog->getId(),$this->oUserCurrent->getId());
$bIsAdministratorBlog=$oBlogUser ? $oBlogUser->getIsAdministrator() : false;
if ($oBlog->getOwnerId()!=$this->oUserCurrent->getId() and !$this->oUserCurrent->isAdministrator() and !$bIsAdministratorBlog) {
$this->Message_AddErrorSingle($this->Lang_Get('system_error'),$this->Lang_Get('error'));
return;
}
/**
* Получаем список пользователей блога (любого статуса)
*/
$aBlogUsers = $this->Blog_GetBlogUsersByBlogId(
$oBlog->getId(),
array(
LsBlog::BLOG_USER_ROLE_REJECT,
LsBlog::BLOG_USER_ROLE_INVITE,
LsBlog::BLOG_USER_ROLE_USER,
LsBlog::BLOG_USER_ROLE_MODERATOR,
LsBlog::BLOG_USER_ROLE_ADMINISTRATOR
)
);
$aUsers=explode(',',$sUsers);
$aResult=array();
/**
* Обрабатываем добавление по каждому из переданных логинов
*/
foreach ($aUsers as $sUser) {
$sUser=trim($sUser);
if ($sUser=='') {
continue;
}
/**
* Если пользователь пытается добавить инвайт
* самому себе, возвращаем ошибку
*/
if(strtolower($sUser)==strtolower($this->oUserCurrent->getLogin())) {
$aResult[]=array(
'bStateError'=>true,
'sMsgTitle'=>$this->Lang_Get('error'),
'sMsg'=>$this->Lang_Get('blog_user_invite_add_self')
);
continue;
}
/**
* Если пользователь не найден или неактивен,
* возвращаем ошибку
*/
if (!$oUser=$this->User_GetUserByLogin($sUser) or $oUser->getActivate()!=1) {
$aResult[]=array(
'bStateError'=>true,
'sMsgTitle'=>$this->Lang_Get('error'),
'sMsg'=>$this->Lang_Get('user_not_found',array('login'=>$sUser)),
'sUserLogin'=>$sUser
);
continue;
}
if(!isset($aBlogUsers[$oUser->getId()])) {
/**
* Создаем нового блог-пользователя со статусом INVITED
*/
$oBlogUserNew=Engine::GetEntity('Blog_BlogUser');
$oBlogUserNew->setBlogId($oBlog->getId());
$oBlogUserNew->setUserId($oUser->getId());
$oBlogUserNew->setUserRole(LsBlog::BLOG_USER_ROLE_INVITE);
if($this->Blog_AddRelationBlogUser($oBlogUserNew)) {
$aResult[]=array(
'bStateError'=>false,
'sMsgTitle'=>$this->Lang_Get('attention'),
'sMsg'=>$this->Lang_Get('blog_user_invite_add_ok',array('login'=>$sUser)),
'sUserLogin'=>$sUser,
'sUserWebPath'=>$oUser->getUserWebPath()
);
$this->SendBlogInvite($oBlog,$oUser);
} else {
$aResult[]=array(
'bStateError'=>true,
'sMsgTitle'=>$this->Lang_Get('error'),
'sMsg'=>$this->Lang_Get('system_error'),
'sUserLogin'=>$sUser
);
}
} else {
/**
* Попытка добавить приглашение уже существующему пользователю,
* возвращаем ошибку (сначала определяя ее точный текст)
*/
switch (true) {
case ($aBlogUsers[$oUser->getId()]->getUserRole()==LsBlog::BLOG_USER_ROLE_INVITE):
$sErrorMessage=$this->Lang_Get('blog_user_already_invited',array('login'=>$sUser));
break;
case ($aBlogUsers[$oUser->getId()]->getUserRole()>LsBlog::BLOG_USER_ROLE_GUEST):
$sErrorMessage=$this->Lang_Get('blog_user_already_exists',array('login'=>$sUser));
break;
case ($aBlogUsers[$oUser->getId()]->getUserRole()==LsBlog::BLOG_USER_ROLE_REJECT):
$sErrorMessage=$this->Lang_Get('blog_user_already_reject',array('login'=>$sUser));
break;
default:
$sErrorMessage=$this->Lang_Get('system_error');
}
$aResult[]=array(
'bStateError'=>true,
'sMsgTitle'=>$this->Lang_Get('error'),
'sMsg'=>$sErrorMessage,
'sUserLogin'=>$sUser
);
continue;
}
}
/**
* Передаем во вьевер массив с результатами обработки по каждому пользователю
*/
$this->Viewer_AssignAjax('aUsers',$aResult);
}
/**
* Выполняет отправку приглашения в блог
* (по внутренней почте и на email)
*
* @param BlogEntity_Blog $oBlog
* @param UserEntity_User $oUser
*/
protected function SendBlogInvite($oBlog,$oUser) {
$sTitle=$this->Lang_Get(
'blog_user_invite_title',
array(
'blog_title'=>$oBlog->getTitle()
)
);
require_once Config::Get('path.root.engine').'/lib/external/XXTEA/encrypt.php';
$sCode=$oBlog->getId().'_'.$oUser->getId();
$sCode=urlencode(base64_encode(xxtea_encrypt($sCode, Config::Get('module.blog.encrypt'))));
$aPath=array(
'accept'=>Router::GetPath('blog').'invite/accept/?code='.$sCode,
'reject'=>Router::GetPath('blog').'invite/reject/?code='.$sCode
);
$sText=$this->Lang_Get(
'blog_user_invite_text',
array(
'login'=>$this->oUserCurrent->getLogin(),
'accept_path'=>$aPath['accept'],
'reject_path'=>$aPath['reject'],
'blog_title'=>$oBlog->getTitle()
)
);
$oTalk=$this->Talk_SendTalk($sTitle,$sText,$this->oUserCurrent,array($oUser),false,false);
/**
* Отправляем пользователю заявку
*/
//$this->Notify_SendBlogUserInvite(
// $oUser,$this->oUserCurrent,$sText,
// Router::GetPath('talk').'read/'.$oTalk->getId().'/'
//);
/**
* Удаляем отправляющего юзера из переписки
*/
$this->Talk_DeleteTalkUserByArray($oTalk->getId(),$this->oUserCurrent->getId());
}
/**
* Обработка отправленого пользователю приглашения вступить в блог
*/
protected function EventInviteBlog() {
require_once Config::Get('path.root.engine').'/lib/external/XXTEA/encrypt.php';
$sCode=xxtea_decrypt(base64_decode(urldecode(getRequest('code'))), Config::Get('module.blog.encrypt'));
list($sBlogId,$sUserId)=explode('_',$sCode,2);
$sAction=$this->GetParam(0);
/**
* Получаем текущего пользователя
*/
if(!$this->User_IsAuthorization()) {
return $this->EventNotFound();
}
$this->oUserCurrent = $this->User_GetUserCurrent();
/**
* Если приглашенный пользователь не является авторизированным
*/
if($this->oUserCurrent->getId()!=$sUserId) {
return $this->EventNotFound();
}
/**
* Получаем указанный блог
*/
if((!$oBlog=$this->Blog_GetBlogById($sBlogId)) || $oBlog->getType()!='close') {
return $this->EventNotFound();
}
/**
* Получаем связь "блог-пользователь" и проверяем,
* чтобы ее тип был INVITE или REJECT
*/
if(!$oBlogUser=$this->Blog_GetBlogUserByBlogIdAndUserId($oBlog->getId(),$this->oUserCurrent->getId())) {
return $this->EventNotFound();
}
if($oBlogUser->getUserRole()>LsBlog::BLOG_USER_ROLE_GUEST) {
$sMessage=$this->Lang_Get('blog_user_invite_already_done');
$this->Message_AddError($sMessage,$this->Lang_Get('error'),true);
Router::Location(Router::GetPath('talk'));
return ;
}
if(!in_array($oBlogUser->getUserRole(),array(LsBlog::BLOG_USER_ROLE_INVITE,LsBlog::BLOG_USER_ROLE_REJECT))) {
$this->Message_AddError($this->Lang_Get('system_error'),$this->Lang_Get('error'),true);
Router::Location(Router::GetPath('talk'));
return ;
}
/**
* Обновляем роль пользователя до читателя
*/
$oBlogUser->setUserRole(($sAction=='accept')?LsBlog::BLOG_USER_ROLE_USER:LsBlog::BLOG_USER_ROLE_REJECT);
if(!$this->Blog_UpdateRelationBlogUser($oBlogUser)) {
$this->Message_AddError($this->Lang_Get('system_error'),$this->Lang_Get('error'),true);
Router::Location(Router::GetPath('talk'));
return ;
}
$sMessage = ($sAction=='accept')
? $this->Lang_Get('blog_user_invite_accept')
: $this->Lang_Get('blog_user_invite_reject');
$this->Message_AddNotice($sMessage,$this->Lang_Get('attention'),true);
Router::Location(Router::GetPath('talk'));
}
/**
* Выполняется при завершении работы экшена
*

View file

@ -220,8 +220,6 @@ class ActionProfile extends Action {
*/
if(!$oUser=$this->User_GetUserById($sUserId)) {
$this->Message_AddError($this->Lang_Get('user_not_found'),$this->Lang_Get('error'),true);
$this->Message_Shutdown();
Router::Location(Router::GetPath('talk'));
return ;
}
@ -243,9 +241,7 @@ class ActionProfile extends Action {
$sMessage=($oFriend)
? $this->Lang_Get('user_friend_offer_already_done')
: $this->Lang_Get('user_friend_offer_not_found');
$this->Message_AddError($sMessage,$this->Lang_Get('error'),true);
$this->Message_Shutdown();
Router::Location(Router::GetPath('talk'));
return ;
@ -273,8 +269,6 @@ class ActionProfile extends Action {
true
);
}
$this->Message_Shutdown();
Router::Location(Router::GetPath('talk'));
}
@ -362,7 +356,7 @@ class ActionProfile extends Action {
/**
* Если пользователь не авторизирован, возвращаем ошибку
*/
*/
if (!$this->User_IsAuthorization()) {
$this->Message_AddErrorSingle(
$this->Lang_Get('need_authorization'),

View file

@ -30,7 +30,15 @@ class LsBlog extends Module {
const BLOG_USER_ROLE_USER = 1;
const BLOG_USER_ROLE_MODERATOR = 2;
const BLOG_USER_ROLE_ADMINISTRATOR = 4;
/**
* Пользователь, приглашенный админом блога в блог
*/
const BLOG_USER_ROLE_INVITE = -1;
/**
* Пользователь, отклонивший приглашение админа
*/
const BLOG_USER_ROLE_REJECT = -2;
protected $oMapperBlog;
protected $oUserCurrent=null;
@ -352,10 +360,14 @@ class LsBlog extends Module {
}
/**
* Получает список пользователей блога
* Получает список пользователей блога.
* Если роль не указана, то считаем что
* поиск производиться по положительным значениям
* (статусом выше GUEST).
*
* @param unknown_type $sBlogId
* @return unknown
* @param string $sBlogId
* @param (null|int|array) $iRole
* @return array
*/
public function GetBlogUsersByBlogId($sBlogId,$iRole=null) {
$aFilter=array(
@ -645,12 +657,14 @@ class LsBlog extends Module {
* (читателем, модератором, или администратором)
*/
$aBlogUsers=$this->GetBlogUsersByUserId($oUser->getId());
foreach ($aBlogUsers as $oBlogUser) {
$oBlog=$oBlogUser->getBlog();
if($oBlogUser->getUserRole()>self::BLOG_USER_ROLE_GUEST) {
$aOpenBlogsUser[$oBlog->getId()]=$oBlog;
}
}
return $aOpenBlogsUser;
}
@ -684,7 +698,6 @@ class LsBlog extends Module {
$aOpenBlogs[]=$oBlog->getId();
}
}
return array_diff($aCloseBlogs,$aOpenBlogs);
}
}

View file

@ -137,7 +137,12 @@ class Mapper_Blog extends Mapper {
$sWhere.=" AND bu.user_id = ".(int)$aFilter['user_id'];
}
if (isset($aFilter['user_role'])) {
$sWhere.=" AND bu.user_role = '".(int)$aFilter['user_role']."'";
if(!is_array($aFilter['user_role'])) {
$aFilter['user_role']=array($aFilter['user_role']);
}
$sWhere.=" AND bu.user_role IN ('".join("', '",$aFilter['user_role'])."')";
} else {
$sWhere.=" AND bu.user_role>".LsBlog::BLOG_USER_ROLE_GUEST;
}
$sql = "SELECT
@ -151,7 +156,7 @@ class Mapper_Blog extends Mapper {
$aBlogUsers=array();
if ($aRows=$this->oDb->select($sql)) {
foreach ($aRows as $aUser) {
$aBlogUsers[]=Engine::GetEntity('Blog_BlogUser',$aUser);
$aBlogUsers[$aUser['user_id']]=Engine::GetEntity('Blog_BlogUser',$aUser);
}
}
return $aBlogUsers;
@ -315,7 +320,7 @@ class Mapper_Blog extends Mapper {
}
public function GetCloseBlogs() {
$sql = "SELECT b.blog_id
$sql = "SELECT b.blog_id
FROM ".Config::Get('db.table.blog')." as b
WHERE b.blog_type='close'
;";

View file

@ -428,7 +428,7 @@ class LsTopic extends Module {
*/
if($this->oUserCurrent) {
$aOpenBlogs = $this->Blog_GetAccessibleBlogsByUser($this->oUserCurrent);
$aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
if(count($aOpenBlogs)) $aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
}
return $this->GetTopicsByFilter($aFilter,$iPage,$iPerPage);
@ -456,7 +456,7 @@ class LsTopic extends Module {
*/
if($this->oUserCurrent) {
$aOpenBlogs = $this->Blog_GetAccessibleBlogsByUser($this->oUserCurrent);
$aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
if(count($aOpenBlogs)) $aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
}
return $this->GetTopicsByFilter($aFilter,$iPage,$iPerPage);
}
@ -480,7 +480,7 @@ class LsTopic extends Module {
*/
if($this->oUserCurrent) {
$aOpenBlogs = $this->Blog_GetAccessibleBlogsByUser($this->oUserCurrent);
$aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
if(count($aOpenBlogs)) $aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
}
$aReturn=$this->GetTopicsByFilter($aFilter,1,$iCount);
if (isset($aReturn['collection'])) {
@ -658,7 +658,7 @@ class LsTopic extends Module {
*/
if($this->oUserCurrent) {
$aOpenBlogs = $this->Blog_GetAccessibleBlogsByUser($this->oUserCurrent);
$aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
if(count($aOpenBlogs)) $aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
}
return $this->GetTopicsByFilter($aFilter,$iPage,$iPerPage);
}
@ -682,7 +682,7 @@ class LsTopic extends Module {
*/
if($this->oUserCurrent) {
$aOpenBlogs = $this->Blog_GetAccessibleBlogsByUser($this->oUserCurrent);
$aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
if(count($aOpenBlogs)) $aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
}
return $this->GetCountTopicsByFilter($aFilter);
}
@ -752,7 +752,7 @@ class LsTopic extends Module {
*/
if($this->oUserCurrent) {
$aOpenBlogs = $this->Blog_GetAccessibleBlogsByUser($this->oUserCurrent);
$aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
if(count($aOpenBlogs)) $aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
}
return $this->GetTopicsByFilter($aFilter,$iPage,$iPerPage);
}
@ -781,7 +781,7 @@ class LsTopic extends Module {
*/
if($this->oUserCurrent) {
$aOpenBlogs = $this->Blog_GetAccessibleBlogsByUser($this->oUserCurrent);
$aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
if(count($aOpenBlogs)) $aFilter['blog_type']['close'] = array_keys($aOpenBlogs);
}
return $this->GetCountTopicsByFilter($aFilter);

View file

@ -282,6 +282,7 @@ $config['module']['blog']['per_page'] = 20; // Число блогов
$config['module']['blog']['personal_good'] = -5; // Рейтинг топика в персональном блоге ниже которого он считается плохим
$config['module']['blog']['collective_good'] = -3; // рейтинг топика в коллективных блогах ниже которого он считается плохим
$config['module']['blog']['index_good'] = -8; // Рейтинг топика выше которого(включительно) он попадает на главную
$config['module']['blog']['encrypt'] = 'livestreet'; // Ключ XXTEA шифрования идентификаторов в ссылках приглашения в блоги
// Модуль Topic
$config['module']['topic']['new_time'] = 60*60*24*1; // Время в секундах в течении которого топик считается новым
$config['module']['topic']['per_page'] = 10; // Число топиков на одну страницу

View file

@ -143,7 +143,20 @@ return array(
'blog_admin_users_submit_ok' => 'Права сохранены',
'blog_admin_users_submit_error' => 'Что-то не так',
'blog_admin_user_add_label' => 'Пригласить пользователей:',
'blog_admin_user_invited' => 'Список приглашенных:',
'blog_close_show' => 'Это закрытый блог, у вас нет прав на просмотр контента',
'blog_user_invite_add_self' => 'Нельзя отправить инвайт самому себе',
'blog_user_invite_add_ok' => 'Пользователю %%login%% отправлено приглашение',
'blog_user_already_invited' => 'Пользователю %%login%% уже отправлен инвайт',
'blog_user_already_exists' => 'Пользователю %%login%% уже состоит в блоге',
'blog_user_already_reject' => 'Пользователю %%login%% отклонил инвайт',
'blog_user_invite_title' => "Приглашение стать читателем блога '%%blog_title%%'",
'blog_user_invite_text' => "Пользователь %%login%% приглашает вас стать читателем закрытого блога '%%blog_title%%'.<br/><br/><a href='%%accept_path%%'>Принять</a> - <a href='%%reject_path%%'>Отклонить</a>",
'blog_user_invite_already_done' => 'Вы уже являетесь пользователем этого блога',
'blog_user_invite_accept' => 'Приглашение принято',
'blog_user_invite_reject' => 'Приглашение отклонено',
/**
* Топики
*/

View file

@ -36,7 +36,6 @@
</form>
{else}
{$aLang.blog_admin_users_empty}
{/if}
{/if}
{include file='footer.tpl'}

View file

@ -0,0 +1,85 @@
<div class="block blogs">
<div class="tl"><div class="tr"></div></div>
<div class="cl"><div class="cr">
{literal}
<script language="JavaScript" type="text/javascript">
document.addEvent('domready', function() {
new Autocompleter.Request.HTML(
$('blog_admin_user_add'),
DIR_WEB_ROOT+'/include/ajax/userAutocompleter.php',
{
'indicatorClass': 'autocompleter-loading',
'minLength': 1,
'selectMode': 'pick',
'multiple': true
}
);
});
function addUserItem(sLogin,sPath) {
if($('invited_list_block').getElements('ul').length==0) {
list=new Element('ul', {class:'list',id:'invited_list'});
$('invited_list_block').adopt(list);
}
oSpan=new Element('span',{'class':'user'});
oLink=new Element('a',{'href':sPath, 'text':sLogin});
oItem=new Element('li');
$('invited_list').adopt(oItem.adopt(oSpan.adopt(oLink)));
}
function addBlogInvite(idBlog) {
sUsers=$('blog_admin_user_add').get('value');
if(sUsers.length<2) {
msgErrorBox.alert('Error','Пользователь не указан');
return false;
}
$('blog_admin_user_add').set('value','');
JsHttpRequest.query(
aRouter['blog']+'ajaxaddbloginvite/',
{ users: sUsers, idBlog: idBlog },
function(result, errors) {
if (!result) {
msgErrorBox.alert('Error','Please try again later');
}
if (result.bStateError) {
msgErrorBox.alert(result.sMsgTitle,result.sMsg);
} else {
var aUsers = result.aUsers;
aUsers.each(function(item,index) {
if(item.bStateError){
msgErrorBox.alert(item.sMsgTitle, item.sMsg);
} else {
addUserItem(item.sUserLogin,item.sUserWebPath);
}
});
}
},
true
);
return false;
}
</script>
{/literal}
<div class="block-content">
<form onsubmit="addBlogInvite({$oBlog->getId()}); return false;">
<p><label for="blog_admin_user_add">{$aLang.blog_admin_user_add_label}</label><br />
<input type="text" id="blog_admin_user_add" name="add" value="" class="w100p" /><br />
</p>
</form>
</div>
<h1>{$aLang.blog_admin_user_invited}</h1>
<div class="block-content" id="invited_list_block">
{if $aBlogUsersInvited}
<ul class="list" id="invited_list">
{foreach from=$aBlogUsersInvited item=oBlogUser}
{assign var='oUser' value=$oBlogUser->getUser()}
<li><span class="user"><a href="{$oUser->getUserWebPath()}">{$oUser->getLogin()}</a></span></li>
{/foreach}
</ul>
{/if}
</div>
<br />
</div></div>
<div class="bl"><div class="br"></div></div>
</div>