Procházet zdrojové kódy

Ticket #4817 - Courses improvements.

AntonLV před 2 roky
rodič
revize
daf3d54da4
26 změnil soubory, kde provedl 1286 přidání a 72 odebrání
  1. 18 7
      modules/base/general/classes/BxBaseModGeneralFormEntry.php
  2. 22 3
      modules/boonex/courses/classes/BxCoursesConfig.php
  3. 150 2
      modules/boonex/courses/classes/BxCoursesDb.php
  4. 40 0
      modules/boonex/courses/classes/BxCoursesFormContentNode.php
  5. 20 3
      modules/boonex/courses/classes/BxCoursesGridCntDataManage.php
  6. 9 4
      modules/boonex/courses/classes/BxCoursesGridCntStructureManage.php
  7. 16 2
      modules/boonex/courses/classes/BxCoursesMenuContentAdd.php
  8. 300 11
      modules/boonex/courses/classes/BxCoursesModule.php
  9. 10 0
      modules/boonex/courses/classes/BxCoursesPageEntry.php
  10. 390 7
      modules/boonex/courses/classes/BxCoursesTemplate.php
  11. 48 7
      modules/boonex/courses/install/langs/en.xml
  12. 1 1
      modules/boonex/courses/install/sql/disable.sql
  13. 19 7
      modules/boonex/courses/install/sql/enable.sql
  14. 33 6
      modules/boonex/courses/install/sql/install.sql
  15. 1 1
      modules/boonex/courses/install/sql/uninstall.sql
  16. 66 0
      modules/boonex/courses/js/entry.js
  17. 17 4
      modules/boonex/courses/template/counters.html
  18. 1 0
      modules/boonex/courses/template/ml2_node_l2.html
  19. 1 0
      modules/boonex/courses/template/ml2_nodes_l2.html
  20. 24 0
      modules/boonex/courses/template/ml3_node_l3.html
  21. 3 0
      modules/boonex/courses/template/ml3_nodes_l3.html
  22. 15 0
      modules/boonex/courses/template/node_l1.html
  23. 33 0
      modules/boonex/courses/template/node_view.html
  24. 4 0
      modules/boonex/courses/template/nodes_l1.html
  25. 45 7
      modules/boonex/russian/data/langs/bx_courses/ru.xml
  26. 0 0
      plugins_public/tailwind/css/tailwind.min.css

+ 18 - 7
modules/base/general/classes/BxBaseModGeneralFormEntry.php

@@ -538,8 +538,8 @@ class BxBaseModGeneralFormEntry extends BxTemplFormView
                 BxDolFormQuery::addFormField($this->id, $aInput['name'], $iContentId, $iAuthor, $this->_oModule->getName());
 
         if(($iContextNid = $this->getCleanValue('context_nid')) !== false)
-            $this->_processContextAfterAdd($iContentId, $iContextNid);
-            
+            $this->_processContextAfterAdd($iContentId, $iContextNid, (int)$this->getCleanValue('context_usage'));
+
         return $iContentId;
     }
 
@@ -1016,16 +1016,27 @@ class BxBaseModGeneralFormEntry extends BxTemplFormView
                 'value' => -abs((int)$iContextId)
             ]);
 
-        if(($iNodeId = bx_get('context_nid')) !== false && !isset($this->aInputs['context_nid'])) {
-            $this->aInputs['context_nid'] = [
-                'name' => 'context_nid',
+        $sKey = 'context_nid';
+        if(($iNodeId = bx_get($sKey)) !== false && !isset($this->aInputs[$sKey])) {
+            $this->aInputs[$sKey] = [
+                'name' => $sKey,
                 'type' => 'hidden',
                 'value' => (int)$iNodeId
             ];
         }
+
+        $sKey = 'context_usage';
+        if(($iUsage = bx_get($sKey)) !== false && !isset($this->aInputs[$sKey])) {
+            $this->aInputs[$sKey] = [
+                'name' => $sKey,
+                'type' => 'hidden',
+                'value' => (int)$iUsage
+            ];
+        }
+        
     }
 
-    protected function _processContextAfterAdd($iContentId, $iContextNid)
+    protected function _processContextAfterAdd($iContentId, $iContextNid, $iContextUsage = 0)
     {
         $CNF = &$this->_oModule->_oConfig->CNF;
 
@@ -1045,7 +1056,7 @@ class BxBaseModGeneralFormEntry extends BxTemplFormView
         $sModule = $oContext->getModule();
         $sMethod = 'on_content_added';
         if(bx_is_srv($sModule, $sMethod))
-            bx_srv($sModule, $sMethod, [$this->MODULE, $iContentId, $oContext->getContentId(), $iContextNid]);
+            bx_srv($sModule, $sMethod, [$this->MODULE, $iContentId, $oContext->getContentId(), $iContextNid, $iContextUsage]);
     }
 
     /**

+ 22 - 3
modules/boonex/courses/classes/BxCoursesConfig.php

@@ -37,7 +37,9 @@ class BxCoursesConfig extends BxBaseModGroupsConfig
             'TABLE_PRICES' => $aModule['db_prefix'] . 'prices',
             'TABLE_CNT_STRUCTURE' => $aModule['db_prefix'] . 'content_structure',
             'TABLE_CNT_NODES' => $aModule['db_prefix'] . 'content_nodes',
+            'TABLE_CNT_NODES2USERS' => $aModule['db_prefix'] . 'content_nodes2users',
             'TABLE_CNT_DATA' => $aModule['db_prefix'] . 'content_data',
+            'TABLE_CNT_DATA2USERS' => $aModule['db_prefix'] . 'content_data2users',
 
             // database fields
             'FIELD_ID' => 'id',
@@ -71,6 +73,7 @@ class BxCoursesConfig extends BxBaseModGroupsConfig
 
             // page URIs
             'URI_VIEW_ENTRY' => 'view-course-profile',
+            'URI_VIEW_ENTRY_NODE' => 'view-course-profile-node',
             'URI_EDIT_ENTRY' => 'edit-course-profile',
             'URI_JOIN_ENTRY' => 'join-course-profile',
             'URI_JOINED_ENTRIES' => 'joined-courses',
@@ -99,7 +102,8 @@ class BxCoursesConfig extends BxBaseModGroupsConfig
 
             'PARAM_CONTENT' => 'bx_courses_enable_content',
             'PARAM_CONTENT_LEVEL_MAX' => 'bx_courses_content_level_max',
-            'PARAM_CONTENT_MODULES' => 'bx_courses_content_modules',
+            'PARAM_CONTENT_MODULES_ST' => 'bx_courses_content_modules_st',
+            'PARAM_CONTENT_MODULES_AT' => 'bx_courses_content_modules_at',
 
             // objects
             'OBJECT_STORAGE' => 'bx_courses_pics',
@@ -277,6 +281,7 @@ class BxCoursesConfig extends BxBaseModGroupsConfig
 
         $this->_aJsClasses = [
             'main' => 'BxCoursesMain',
+            'entry' => 'BxCoursesEntry',
             'manage_tools' => 'BxCoursesManageTools',
             'invite_popup' => 'BxCoursesInvitePopup',
             'prices' => 'BxCoursesPrices'
@@ -284,6 +289,7 @@ class BxCoursesConfig extends BxBaseModGroupsConfig
 
         $this->_aJsObjects = [
             'main' => 'oBxCoursesMain',
+            'entry' => 'oBxCoursesEntry',
             'manage_tools' => 'oBxCoursesManageTools',
             'invite_popup' => 'oBxCoursesInvitePopup',
             'prices' => 'oBxCoursesPrices'
@@ -310,9 +316,22 @@ class BxCoursesConfig extends BxBaseModGroupsConfig
         return (int)getParam($this->CNF['PARAM_CONTENT_LEVEL_MAX']);
     }
 
-    public function getContentModules()
+    public function getContentModules($iUsage)
     {
-        return explode(',', getParam($this->CNF['PARAM_CONTENT_MODULES']));
+        return explode(',', getParam($this->CNF['PARAM_CONTENT_MODULES_' . $this->getUsageI2S($iUsage, false)]));
+    }
+
+    public function getUsageI2S($iUsage, $bLowerCase = true)
+    {
+        $a = [
+            BX_COURSES_CND_USAGE_ST => 'st',
+            BX_COURSES_CND_USAGE_AT => 'at'
+        ];
+
+        if(!isset($a[$iUsage]))
+            $iUsage = BX_COURSES_CND_USAGE_ST;        
+
+        return $bLowerCase ? $a[$iUsage] : strtoupper($a[$iUsage]);
     }
 }
 

+ 150 - 2
modules/boonex/courses/classes/BxCoursesDb.php

@@ -44,9 +44,9 @@ class BxCoursesDb extends BxBaseModGroupsDb
                     'id' => $aParams['id']
                 ];
 
-                $sSelectClause .= ", `tcs`.*";
-                $sWhereClause = "AND `tcn`.`id`=:id";
+                $sSelectClause .= ", `tcs`.`parent_id`, `tcs`.`level`, `tcs`.`order`, `tcs`.`cn_l2`, `tcs`.`cn_l3`";
                 $sJoinClause = "LEFT JOIN `" . $CNF['TABLE_CNT_STRUCTURE'] . "` AS `tcs` ON `tcn`.`id`=`tcs`.`node_id`";
+                $sWhereClause = "AND `tcn`.`id`=:id";
                 break;
         }
 
@@ -70,6 +70,26 @@ class BxCoursesDb extends BxBaseModGroupsDb
         return $this->query("UPDATE `" . $CNF['TABLE_CNT_NODES'] . "` SET " . $this->arrayToSQL($aParamsSet) . " WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
     }
 
+    public function insertContentNodes2Users($aParamsSet)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet))
+            return false;
+
+        return (int)$this->query("INSERT INTO `" . $CNF['TABLE_CNT_NODES2USERS'] . "` SET " . $this->arrayToSQL($aParamsSet) . " ON DUPLICATE KEY UPDATE `date`=UNIX_TIMESTAMP()") > 0 ? (int)$this->lastId() : false;
+    }
+
+    public function deleteContentNodes2Users($aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsWhere))
+            return false;
+
+        return $this->query("DELETE FROM `" . $CNF['TABLE_CNT_NODES2USERS'] . "` WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
+    }
+
     public function getContentStructure($aParams)
     {
         $CNF = &$this->_oConfig->CNF;
@@ -97,6 +117,67 @@ class BxCoursesDb extends BxBaseModGroupsDb
 
                 $sWhereClause = "AND `tcs`.`node_id`=:node_id";
                 break;
+
+            case 'entry_id_full':
+                $aMethod['params'][1] = [
+                    'entry_id' => $aParams['entry_id']
+                ];
+
+                $sSelectClause .= ", `tcn`.`title`, `tcn`.`counters`";
+                $sJoinClause = "LEFT JOIN `" . $CNF['TABLE_CNT_NODES'] . "` AS `tcn` ON `tcs`.`node_id`=`tcn`.`id`";
+                $sWhereClause = "AND `tcs`.`entry_id`=:entry_id";
+
+                if(isset($aParams['parent_id'])) {
+                    $aMethod['params'][1]['parent_id'] = $aParams['parent_id'];
+                        
+                    $sWhereClause .= " AND `tcs`.`parent_id`=:parent_id";
+                }
+
+                if(isset($aParams['level'])) {
+                    $aMethod['params'][1]['level'] = $aParams['level'];
+                        
+                    $sWhereClause .= " AND `tcs`.`level`=:level";
+                }
+
+                if(isset($aParams['status'])) {
+                    $aMethod['params'][1]['status'] = $aParams['status'];
+                        
+                    $sWhereClause .= " AND `tcn`.`status`=:status";
+                }
+
+                if(isset($aParams['start'], $aParams['per_page']) && (int)$aParams['per_page'] != 0)
+                    $sLimitClause = $aParams['start'] . ', ' . $aParams['per_page'];
+                break;
+                
+            case 'parent_id':
+                $aMethod['params'][1] = [
+                    'parent_id' => $aParams['parent_id']
+                ];
+
+                $sWhereClause = "AND `tcs`.`parent_id`=:parent_id";
+                break;
+
+            case 'user_passed':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'profile_id' => $aParams['profile_id'],
+                    'node_id' => $aParams['node_id']
+                ];
+
+                $sSelectClause = "`tcnu`.*";
+                $sJoinClause = "INNER JOIN `" . $CNF['TABLE_CNT_NODES2USERS'] . "` AS `tcnu` ON `tcs`.`node_id`=`tcnu`.`node_id` AND `tcnu`.`node_id`=:node_id AND `tcnu`.`profile_id`=:profile_id";
+                break;
+
+            case 'user_track':
+                $aMethod['params'][1] = [
+                    'profile_id' => $aParams['profile_id'],
+                    'entry_id' => $aParams['entry_id'],
+                    'node_id' => $aParams['node_id']
+                ];
+
+                $sJoinClause = "INNER JOIN `" . $CNF['TABLE_CNT_NODES2USERS'] . "` AS `tcnu` ON `tcs`.`node_id`=`tcnu`.`node_id` AND `tcnu`.`profile_id`=:profile_id";
+                $sWhereClause = "AND `tcs`.`entry_id`=:entry_id AND `tcs`.`parent_id`=:node_id";
+                break;
         }
 
         if(!empty($sOrderClause))
@@ -190,6 +271,53 @@ class BxCoursesDb extends BxBaseModGroupsDb
 
                 $sWhereClause = "AND `tcd`.`content_type`=:content_type AND `tcd`.`content_id`=:content_id";
                 break;
+
+            case 'node_id':
+                $aMethod['params'][1] = [
+                    'node_id' => $aParams['node_id']
+                ];
+
+                $sWhereClause = "AND `tcd`.`node_id`=:node_id";
+                $sOrderClause = "`tcd`.`order` ASC";
+                break;
+
+            case 'entry_node_ids':
+                $aMethod['params'][1] = [
+                    'entry_id' => $aParams['entry_id'],
+                    'node_id' => $aParams['node_id']
+                ];
+
+                $sWhereClause = "AND `tcd`.`entry_id`=:entry_id AND `tcd`.`node_id`=:node_id";
+                $sOrderClause = "`tcd`.`order` ASC";
+
+                if(isset($aParams['usage'])) {
+                    $aMethod['params'][1]['usage'] = $aParams['usage'];
+
+                    $sWhereClause .= " AND `tcd`.`usage`=:usage";
+                }
+                break;
+
+            case 'user_passed':
+                $aMethod['name'] = 'getRow';
+                $aMethod['params'][1] = [
+                    'profile_id' => $aParams['profile_id'],
+                    'data_id' => $aParams['data_id']
+                ];
+
+                $sSelectClause = "`tcdu`.*";
+                $sJoinClause = "INNER JOIN `" . $CNF['TABLE_CNT_DATA2USERS'] . "` AS `tcdu` ON `tcd`.`id`=`tcdu`.`data_id` AND `tcdu`.`data_id`=:data_id AND `tcdu`.`profile_id`=:profile_id";
+                break;
+
+            case 'user_track':
+                $aMethod['params'][1] = [
+                    'profile_id' => $aParams['profile_id'],
+                    'entry_id' => $aParams['entry_id'],
+                    'node_id' => $aParams['node_id']
+                ];
+
+                $sJoinClause = "INNER JOIN `" . $CNF['TABLE_CNT_DATA2USERS'] . "` AS `tcdu` ON `tcd`.`id`=`tcdu`.`data_id` AND `tcdu`.`profile_id`=:profile_id";
+                $sWhereClause = "AND `tcd`.`entry_id`=:entry_id AND `tcd`.`node_id`=:node_id AND `tcd`.`usage`='" . BX_COURSES_CND_USAGE_ST . "'";
+                break;
         }
 
         if(!empty($sOrderClause))
@@ -231,6 +359,26 @@ class BxCoursesDb extends BxBaseModGroupsDb
 
         return $this->query("DELETE FROM `" . $CNF['TABLE_CNT_DATA'] . "` WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
     }
+
+    public function insertContentData2Users($aParamsSet)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsSet))
+            return false;
+
+        return (int)$this->query("INSERT INTO `" . $CNF['TABLE_CNT_DATA2USERS'] . "` SET " . $this->arrayToSQL($aParamsSet) . " ON DUPLICATE KEY UPDATE `date`=UNIX_TIMESTAMP()") > 0 ? (int)$this->lastId() : false;
+    }
+
+    public function deleteContentData2Users($aParamsWhere)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParamsWhere))
+            return false;
+
+        return $this->query("DELETE FROM `" . $CNF['TABLE_CNT_DATA2USERS'] . "` WHERE " . $this->arrayToSQL($aParamsWhere, " AND "));
+    }
 }
 
 /** @} */

+ 40 - 0
modules/boonex/courses/classes/BxCoursesFormContentNode.php

@@ -14,12 +14,52 @@ class BxCoursesFormContentNode extends BxTemplFormView
     protected $_sModule;
     protected $_oModule;
 
+    protected $_iParentId;
+    protected $_iLevel;
+    protected $_iLevelMax;
+    protected $_aLevelMaxInputs;
+
     public function __construct($aInfo, $oTemplate = false)
     {
         $this->_sModule = 'bx_courses';
         $this->_oModule = BxDolModule::getInstance($this->_sModule);
 
         parent::__construct($aInfo, $oTemplate);
+
+        $this->_iParentId = 0;
+        $this->_iLevel = 0;
+        $this->_iLevelMax = $this->_oModule->_oConfig->isContentLevelMax();
+        $this->_aLevelMaxInputs = ['text', 'passing'];
+        
+        if(isset($this->aInputs['passing']))
+            $this->aInputs['passing']['values'] = [
+                ['key' => 0, 'value' => _t('_bx_courses_form_content_node_input_passing_all')],
+                ['key' => 1, 'value' => _t('_bx_courses_form_content_node_input_passing_alternate')]
+            ];
+    }
+
+    public function setData($iParentId, $iLevel = 0)
+    {
+        $this->_iParentId = $iParentId;
+
+        if(!$iLevel) {
+            $aParent = $this->_oModule->_oDb->getContentStructure([
+                'sample' => 'node_id', 
+                'node_id' => $this->_iParentId
+            ]);
+
+            $iLevel = $this->_oModule->getNodeLevelByParent($aParent);
+        }
+
+        $this->_iLevel = $iLevel;
+    }
+
+    public function initChecker($aValues = [], $aSpecificValues = [])
+    {
+        if($this->_iLevel != $this->_iLevelMax)
+            $this->aInputs = array_diff_key($this->aInputs, array_flip($this->_aLevelMaxInputs));
+
+        parent::initChecker ($aValues, $aSpecificValues);
     }
 }
 

+ 20 - 3
modules/boonex/courses/classes/BxCoursesGridCntDataManage.php

@@ -82,6 +82,8 @@ class BxCoursesGridCntDataManage extends BxTemplGrid
             $mixedValue = bx_srv($sModule, $sMethod, [$iContentId]);
         if(!$mixedValue && ($sMethod = 'get_text') &&  bx_is_srv($sModule, $sMethod))
             $mixedValue = bx_srv($sModule, $sMethod, [$iContentId]);
+        if(!$mixedValue)
+            $mixedValue = _t('_undefined');
 
         if(($sMethod = 'get_link') && bx_is_srv($sModule, $sMethod))
             $mixedValue = $this->_oModule->_oTemplate->parseHtmlByName('name_link.html', [
@@ -96,7 +98,12 @@ class BxCoursesGridCntDataManage extends BxTemplGrid
     protected function _getCellContentType($mixedValue, $sKey, $aField, $aRow)
     {
         return parent::_getCellDefault(_t('_' . $mixedValue), $sKey, $aField, $aRow);
-    }    
+    }
+
+    protected function _getCellUsage($mixedValue, $sKey, $aField, $aRow)
+    {
+        return parent::_getCellDefault(_t('_bx_courses_txt_data_usage_' . $mixedValue), $sKey, $aField, $aRow);
+    }
 
     protected function _getCellAdded($mixedValue, $sKey, $aField, $aRow)
     {
@@ -122,12 +129,22 @@ class BxCoursesGridCntDataManage extends BxTemplGrid
     	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
     }
 
-    protected function _getActionAdd($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
+    protected function _getActionAddSt($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
+    {
+        $CNF = &$this->_oModule->_oConfig->CNF;
+
+        $a['attr'] = array_merge($a['attr'], [
+            "onclick" => "$(this).off('click'); bx_menu_popup('" . $CNF['OBJECT_MENU_CONTENT_ADD'] . "', this, {removeOnClose: 1}, {entry_pid: " . $this->_iEntryPid . ", node_id: " . $this->_iNodeId . ", usage:0});"
+    	]);
+    	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
+    }
+
+    protected function _getActionAddAt($sType, $sKey, $a, $isSmall = false, $isDisabled = false, $aRow = [])
     {
         $CNF = &$this->_oModule->_oConfig->CNF;
 
         $a['attr'] = array_merge($a['attr'], [
-            "onclick" => "$(this).off('click'); bx_menu_popup('" . $CNF['OBJECT_MENU_CONTENT_ADD'] . "', this, {}, {entry_pid: " . $this->_iEntryPid . ", node_id: " . $this->_iNodeId . "});"
+            "onclick" => "$(this).off('click'); bx_menu_popup('" . $CNF['OBJECT_MENU_CONTENT_ADD'] . "', this, {removeOnClose: 1}, {entry_pid: " . $this->_iEntryPid . ", node_id: " . $this->_iNodeId . ", usage:1});"
     	]);
     	return $this->_getActionDefault ($sType, $sKey, $a, $isSmall, $isDisabled, $aRow);
     }

+ 9 - 4
modules/boonex/courses/classes/BxCoursesGridCntStructureManage.php

@@ -44,9 +44,9 @@ class BxCoursesGridCntStructureManage extends BxTemplGrid
         $this->_iLevelMax = $this->_oModule->_oConfig->isContentLevelMax();
 
         $this->_aLevelToNode = [
-            1 => _t('_bx_courses_txt_module'),
-            2 => _t('_bx_courses_txt_theme'),
-            3 => _t('_bx_courses_txt_lesson')
+            1 => _t('_bx_courses_txt_sample_l1_single'),
+            2 => _t('_bx_courses_txt_sample_l2_single'),
+            3 => _t('_bx_courses_txt_sample_l3_single')
         ];
     }
 
@@ -96,6 +96,7 @@ class BxCoursesGridCntStructureManage extends BxTemplGrid
     	$oForm->setId($sForm);
         $oForm->setName($sForm);
         $oForm->setAction(BX_DOL_URL_ROOT . bx_append_url_params('grid.php', ['o' => $this->_sObject, 'a' => $sAction, 'entry_id' => $this->_iEntryId, 'parent_id' => $this->_iParentId]));
+        $oForm->setData($this->_iParentId, $this->_iLevel);
 
         $oForm->initChecker();
         if($oForm->isSubmittedAndValid()) {
@@ -155,6 +156,7 @@ class BxCoursesGridCntStructureManage extends BxTemplGrid
         $oForm->setId($sForm);
         $oForm->setName($sForm);
     	$oForm->setAction(BX_DOL_URL_ROOT . bx_append_url_params('grid.php', ['o' => $this->_sObject, 'a' => $sAction, 'entry_id' => $this->_iEntryId, 'parent_id' => $this->_iParentId, 'id' => $aNode['id']]));
+        $oForm->setData($this->_iParentId);
 
         $oForm->initChecker($aNode);
         if($oForm->isSubmittedAndValid()) {
@@ -275,6 +277,9 @@ class BxCoursesGridCntStructureManage extends BxTemplGrid
 
     protected function _delete($mixedId)
     {
+        if($this->_iLevel > 1) 
+            $this->_oModule->_oDb->updateContentStructureCounters($this->_iParentId, $this->_iLevel, -1);
+
         $this->_oModule->_oDb->deleteContentStructureNode(['node_id' => (int)$mixedId]);
 
         return parent::_delete($mixedId);
@@ -302,7 +307,7 @@ class BxCoursesGridCntStructureManage extends BxTemplGrid
 
     protected function _getNodeLevel()
     {
-        return (is_array($this->_aParentInfo) && !empty($this->_aParentInfo['level']) ? (int)$this->_aParentInfo['level'] : 0) + 1;
+        return $this->_oModule->getNodeLevelByParent($this->_aParentInfo);
     }
     
     protected function _parseNodeName($s)

+ 16 - 2
modules/boonex/courses/classes/BxCoursesMenuContentAdd.php

@@ -16,6 +16,7 @@ class BxCoursesMenuContentAdd extends BxTemplMenu
     
     protected $_iEntryPid;
     protected $_iNodeId;
+    protected $_iUsage;
 
     public function __construct($aObject, $oTemplate = false)
     {
@@ -31,6 +32,10 @@ class BxCoursesMenuContentAdd extends BxTemplMenu
         $this->_iNodeId = 0;
         if(($iNodeId = bx_get('node_id')) !== false)
             $this->_iNodeId = (int)$iNodeId;
+        
+        $this->_iUsage = 0;
+        if(($iUsage = bx_get('usage')) !== false)
+            $this->_iUsage = (int)$iUsage;
     }
 
     protected function getMenuItemsRaw()
@@ -39,7 +44,7 @@ class BxCoursesMenuContentAdd extends BxTemplMenu
 
         $oPermalink = BxDolPermalinks::getInstance();
 
-        $aModules = $this->_oModule->_oConfig->getContentModules();
+        $aModules = $this->_oModule->_oConfig->getContentModules($this->_iUsage);
         foreach($aModules as $sModule) {
             $oModule = BxDolModule::getInstance($sModule);
             if(!$oModule)
@@ -52,9 +57,18 @@ class BxCoursesMenuContentAdd extends BxTemplMenu
             $sUrl = $oPermalink->permalink('page.php?i=' . $CNF['URI_ADD_ENTRY'], [
                 'context_pid' => $this->_iEntryPid,
                 'context_nid' => $this->_iNodeId,
+                'context_usage' => $this->_iUsage
             ]);
 
-            $aResults[] = ['id' => $sModule, 'name' => $sModule, 'class' => '', 'link' => $sUrl, 'target' => '_self', 'title' => _t('_' . $sModule)];
+            $aResults[] = [
+                'id' => $sModule, 
+                'name' => $sModule, 
+                'class' => '', 
+                'link' => $sUrl, 
+                'onclick' => "$(this).parents('.bx-popup-applied:visible:first').dolPopupHide()",
+                'target' => '_blank', 
+                'title' => _t('_' . $sModule)
+            ];
         }
 
         return $aResults;

+ 300 - 11
modules/boonex/courses/classes/BxCoursesModule.php

@@ -9,6 +9,14 @@
  * @{
  */
 
+/**
+ * Content data usage:
+ * st - step,
+ * at - attachment
+ */
+define('BX_COURSES_CND_USAGE_ST', 0);
+define('BX_COURSES_CND_USAGE_AT', 1);
+
 /**
  * Courses profiles module.
  */
@@ -21,16 +29,160 @@ class BxCoursesModule extends BxBaseModGroupsModule
         $this->_aSearchableNamesExcept[] = $this->_oConfig->CNF['FIELD_JOIN_CONFIRMATION'];
     }
 
-    public function serviceGetOptionsContentModules()
+    public function actionPassNode()
     {
-        $aResult = [];
+        $iNodeId = bx_get('node_id');
+        if($iNodeId === false)
+            return echoJson([]);
 
-        $aModules = bx_srv('system', 'get_modules_by_type', ['content']);
-        foreach($aModules as $aModule)
-            $aResult[] = ['key' => $aModule['name'], 'value' => $aModule['title']];
+        return echoJson($this->servicePassNode((int)$iNodeId));
+    }
+
+    public function actionPassData()
+    {
+        $iDataId = bx_get('data_id');
+        if($iDataId === false)
+            return echoJson([]);
+
+        return echoJson($this->servicePassData((int)$iDataId));
+    }
+
+    public function serviceGetSafeServices()
+    {
+        return array_merge(parent::serviceGetSafeServices(), [
+            'PassNode' => '',
+            'PassData' => '',
+        ]);
+    }
+
+    public function servicePassNode($iNodeId)
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        $aNode = $this->_oDb->getContentNodes([
+            'sample' => 'id',
+            'id' => $iNodeId
+        ]);
+
+        if(empty($aNode) || !is_array($aNode))
+            return ['code' => 1];
+
+        $iEntryId = (int)$aNode['entry_id'];
+        $iProfileId = bx_get_logged_profile_id();
+        if($this->isNodePassed($iProfileId, $iNodeId)) {
+            $aDataItems = $this->_oDb->getContentData([
+                'sample' => 'entry_node_ids', 
+                'entry_id' => $iEntryId,
+                'node_id' => $iNodeId,
+            ]);
+
+            if(!empty($aDataItems) && is_array($aDataItems))
+                foreach($aDataItems as $aDataItem)
+                    $this->_oDb->deleteContentData2Users(['data_id' => $aDataItem['id'], 'profile_id' => $iProfileId]);
+
+            $this->_oDb->deleteContentNodes2Users(['node_id' => $iNodeId, 'profile_id' => $iProfileId]);
+        }
+
+        return [
+            'code' => 0, 
+            'redirect' => BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY_NODE'] . '&id=' . $iEntryId, [
+                'node_id' => $iNodeId
+            ])
+        ];
+    }
+
+    public function servicePassData($iDataId)
+    {
+        $iNow = time();
+        $iProfileId = bx_get_logged_profile_id();
+        
+        $aData = $this->_oDb->getContentData([
+            'sample' => 'id',
+            'id' => $iDataId
+        ]);
+        
+        if(empty($aData) || !is_array($aData))
+            return ['code' => 1];
+
+        $this->_oDb->insertContentData2Users([
+            'data_id' => $iDataId, 
+            'profile_id' => $iProfileId, 
+            'date' => $iNow
+        ]);
+
+        $aNode = $this->_oDb->getContentNodes([
+            'sample' => 'id_full',
+            'id' => $aData['node_id']
+        ]);
+
+        $iTotal = $this->getDataTotalByNode($aNode, BX_COURSES_CND_USAGE_ST);
+
+        $aUserTrack = $this->_oDb->getContentData([
+            'sample' => 'user_track', 
+            'entry_id' => $aNode['entry_id'], 
+            'node_id' => $aNode['id'], 
+            'profile_id' => $iProfileId
+        ]);
+
+        if(count($aUserTrack) == $iTotal) {
+            $this->_oDb->insertContentNodes2Users([
+                'node_id' => $aNode['id'], 
+                'profile_id' => $iProfileId, 
+                'date' => $iNow
+            ]);
+            
+            $this->_autoPassNodes($iProfileId, $aNode['parent_id']);
+        }
+
+        $aResult = ['code' => 0];
+        if(($sMethod = 'get_link') && bx_is_srv($aData['content_type'], $sMethod))
+            $aResult['redirect'] = bx_srv($aData['content_type'], $sMethod, [$aData['content_id']]);
 
         return $aResult;
     }
+    
+    protected function _autoPassNodes($iProfileId, $iNodeId)
+    {
+        $aNode = $this->_oDb->getContentStructure([
+            'sample' => 'node_id', 
+            'node_id' => $iNodeId, 
+        ]);
+
+        if(empty($aNode) || !is_array($aNode))
+            return;
+
+        $iTotal = (int)$aNode['cn_l' . ($aNode['level'] + 1)];
+        if(!$iTotal)
+            return;
+
+        $aUserTrack = $this->_oDb->getContentStructure([
+            'sample' => 'user_track',
+            'profile_id' => $iProfileId,
+            'entry_id' => $aNode['entry_id'],
+            'node_id' => $iNodeId
+        ]);
+
+        if(count($aUserTrack) == $iTotal) {
+            $this->_oDb->insertContentNodes2Users([
+                'node_id' => $iNodeId, 
+                'profile_id' => $iProfileId, 
+                'date' => time()
+            ]);
+
+            if((int)$aNode['parent_id'] != 0)
+                $this->_autoPassNodes($iProfileId, $aNode['parent_id']);
+        }
+    }
+
+    public function serviceGetOptionsContentModulesSt()
+    {
+        return $this->_getOptionsContentModules();
+    }
+
+    public function serviceGetOptionsContentModulesAt()
+    {
+        return $this->_getOptionsContentModules();
+    }
 
     public function serviceIsContentAvaliable($iProfileId)
     {
@@ -44,13 +196,14 @@ class BxCoursesModule extends BxBaseModGroupsModule
         return true;
     }
 
-    public function serviceOnContentAdded($sContentType, $iContentId, $iContextId, $iContextNodeId)
+    public function serviceOnContentAdded($sContentType, $iContentId, $iContextId, $iContextNodeId, $iContextUsage)
     {
         $this->_oDb->insertContentData([
             'entry_id' => $iContextId,
             'node_id' => $iContextNodeId,
             'content_type' => $sContentType,
             'content_id' => $iContentId,
+            'usage' => $iContextUsage,
             'added' => time(),
             'order' => $this->_oDb->getContentDataOrderMax($iContextId, $iContextNodeId)
         ]);
@@ -61,9 +214,13 @@ class BxCoursesModule extends BxBaseModGroupsModule
         if(!empty($aNode['counters']))
             $aCounters = json_decode($aNode['counters'], true);
 
-        if(!isset($aCounters[$sContentType]))
-            $aCounters[$sContentType] = 0;
-        $aCounters[$sContentType] += 1;
+        $sUsage = $this->_oConfig->getUsageI2S($iContextUsage);
+        if(!isset($aCounters[$sUsage]))
+            $aCounters[$sUsage] = [];
+
+        if(!isset($aCounters[$sUsage][$sContentType]))
+            $aCounters[$sUsage][$sContentType] = 0;
+        $aCounters[$sUsage][$sContentType] += 1;
 
         $this->_oDb->updateContentNodes(['counters' => json_encode($aCounters)], ['id' => $iContextNodeId]);
     }
@@ -82,9 +239,14 @@ class BxCoursesModule extends BxBaseModGroupsModule
 
         $aNode = $this->_oDb->getContentNodes(['sample' => 'id', 'id' => $aData['node_id']]);
         if(!empty($aNode['counters'])) {
+            $sUsage = $this->_oConfig->getUsageI2S($aData['usage']);
             $aCounters = json_decode($aNode['counters'], true);
-            if(!empty($aCounters[$sContentType])) {
-                $aCounters[$sContentType] -= 1;
+
+            if(!empty($aCounters[$sUsage][$sContentType])) {
+                if((int)$aCounters[$sUsage][$sContentType] > 1)
+                    $aCounters[$sUsage][$sContentType] -= 1;
+                else
+                    unset($aCounters[$sUsage][$sContentType]);
 
                 $this->_oDb->updateContentNodes(['counters' => json_encode($aCounters)], ['id' => $aData['node_id']]);
             }
@@ -137,6 +299,133 @@ class BxCoursesModule extends BxBaseModGroupsModule
         return $oGrid->getCode();
     }
     
+    public function serviceEntityStructureL1Block($iContentId = 0)
+    {
+        if(!$this->_oConfig->isContent())
+            return '';
+
+        return $this->_serviceTemplateFuncEx ('entryStructureByLevel', $iContentId, [
+            'level' => 1,
+            'selected' => ($iNodeId = bx_get('parent_id')) !== false ? (int)$iNodeId : 0,
+            'start' => ($iStart = bx_get('start')) !== false ? (int)$iStart : 0,
+            'per_page' => ($iPerPage = bx_get('per_page')) !== false ? (int)$iPerPage : 0,
+        ]);
+    }
+
+    public function serviceEntityStructureL2Block($iContentId = 0, $iParentId = 0)
+    {
+        if(!$this->_oConfig->isContent())
+            return '';
+
+        if(!$iParentId && ($_iParentId = bx_get('parent_id')) !== false)
+            $iParentId = (int)$_iParentId;
+
+        return $this->_serviceTemplateFuncEx ('entryStructureByParentMl3', $iContentId, [
+            'parent_id' => $iParentId
+        ]);
+    }
+    
+    public function serviceEntityNodeBlock($iContentId = 0, $iNodeId = 0, $iUsage = false)
+    {
+        if(!$this->_oConfig->isContent())
+            return '';
+
+        if(!$iNodeId && ($_iNodeId = bx_get('node_id')) !== false)
+            $iNodeId = (int)$_iNodeId;
+
+        if($iUsage === false && ($_iUsage = bx_get('usage')) !== false)
+            $iUsage = (int)$_iUsage;
+
+        return $this->_serviceTemplateFuncEx ('entryNode', $iContentId, [
+            'node_id' => $iNodeId,
+            'usage' => $iUsage
+        ]);
+    }
+    
+    public function getNodeLevelByParent($aParentInfo)
+    {
+        return (is_array($aParentInfo) && !empty($aParentInfo['level']) ? (int)$aParentInfo['level'] : 0) + 1;
+    }
+    
+    public function getDataTotalByNode($mixedNode, $iUsage = 0)
+    {
+        if(!is_array($mixedNode))
+            $mixedNode = $this->_oDb->getContentNodes([
+                'sample' => 'id', 
+                'id' => (int)$mixedNode
+            ]);
+        
+        $iTotal = 0;
+        $sUsage = $this->_oConfig->getUsageI2S($iUsage);
+        if(!empty($mixedNode['counters']) && ($aCountes = json_decode($mixedNode['counters'], true)) && !empty($aCountes[$sUsage]) && is_array($aCountes[$sUsage]))
+            $iTotal = array_sum($aCountes[$sUsage]);
+
+        return $iTotal;
+    }
+
+    public function isNodeStarted($iProfileId, $mixedNode)
+    {
+        $iNodeId = is_array($mixedNode) ? $mixedNode['id'] : (int)$mixedNode;
+
+        $aChildNodes = $this->_oDb->getContentStructure([
+            'sample' => 'parent_id',
+            'parent_id' => $iNodeId
+        ]);
+
+        if(!empty($aChildNodes) && is_array($aChildNodes)) {
+            foreach($aChildNodes as $aChildNode)
+                if($this->isNodePassed($iProfileId, $aChildNode['node_id']))
+                    return true;
+                else
+                    return $this->isNodeStarted($iProfileId, $aChildNode['node_id']);
+        }
+        else {
+            $aChildData = $this->_oDb->getContentData([
+                'sample' => 'node_id',
+                'node_id' => $iNodeId
+            ]);
+
+            if(!empty($aChildData) && is_array($aChildData))
+                foreach($aChildData as $aChildData)
+                    if($this->isDataPassed($iProfileId, $aChildData['id']))
+                        return true;
+
+            return false;
+        }
+    }
+
+    public function isNodePassed($iProfileId, $mixedNode)
+    {
+        $aUserTrack = $this->_oDb->getContentStructure([
+            'sample' => 'user_passed', 
+            'profile_id' => $iProfileId, 
+            'node_id' => is_array($mixedNode) ? $mixedNode['id'] : (int)$mixedNode
+        ]);
+
+        return !empty($aUserTrack) && is_array($aUserTrack);
+    }
+    
+    public function isDataPassed($iProfileId, $mixedData)
+    {
+        $aUserTrack = $this->_oDb->getContentData([
+            'sample' => 'user_passed', 
+            'profile_id' => $iProfileId, 
+            'data_id' => is_array($mixedData) ? $mixedData['id'] : (int)$mixedData
+        ]);
+
+        return !empty($aUserTrack) && is_array($aUserTrack);
+    }
+
+    protected function _getOptionsContentModules()
+    {
+        $aResult = [];
+
+        $aModules = bx_srv('system', 'get_modules_by_type', ['content']);
+        foreach($aModules as $aModule)
+            $aResult[] = ['key' => $aModule['name'], 'value' => $aModule['title']];
+
+        return $aResult;
+    }
 }
 
 /** @} */

+ 10 - 0
modules/boonex/courses/classes/BxCoursesPageEntry.php

@@ -19,6 +19,16 @@ class BxCoursesPageEntry extends BxBaseModGroupsPageEntry
         $this->MODULE = 'bx_courses';
         parent::__construct($aObject, $oTemplate);
     }
+
+    public function getCode ()
+    {
+        $sResult = parent::getCode();
+        if(!empty($sResult))
+            $sResult .= $this->_oModule->_oTemplate->getJsCode('entry');
+
+        $this->_oModule->_oTemplate->addJs(['modules/base/groups/js/|entry.js', 'entry.js']);
+        return $sResult;
+    }
 }
 
 /** @} */

+ 390 - 7
modules/boonex/courses/classes/BxCoursesTemplate.php

@@ -22,16 +22,399 @@ class BxCoursesTemplate extends BxBaseModGroupsTemplate
 
     public function getCounters($aCounters)
     {
-        $aTmplVarsCountes = [];
-        foreach($aCounters as $sModule => $iCount)
-            $aTmplVarsCountes[] = [
-                'title' => _t('_' . $sModule), 
-                'value' => $iCount
+        $aTmplVars = [];
+        foreach([BX_COURSES_CND_USAGE_ST, BX_COURSES_CND_USAGE_AT] as $iUsage) {
+            $sUsage = $this->_oConfig->getUsageI2S($iUsage);
+
+            $aTmplVarsCounters = [true];
+            $bTmplVarsCounters = !empty($aCounters[$sUsage]) && is_array($aCounters[$sUsage]);
+            if($bTmplVarsCounters) {
+                $aTmplVarsCounters['bx_repeat:counters_' . $sUsage] = [];
+                foreach($aCounters[$sUsage] as $sModule => $iCount)
+                    $aTmplVarsCounters['bx_repeat:counters_' . $sUsage][] = [
+                        'title' => _t('_' . $sModule), 
+                        'value' => $iCount
+                    ];
+            }
+
+            $aTmplVars['bx_if:show_' . $sUsage] = [
+                'condition' => $bTmplVarsCounters,
+                'content' => $aTmplVarsCounters
+            ];
+        }
+
+        return $this->parseHtmlByName('counters.html', $aTmplVars);
+    }
+    
+    public function entryStructureByLevel($aContentInfo, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($aParams['level']))
+            return '';
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $iProfileId = bx_get_logged_profile_id();
+
+        $oPermalink = BxDolPermalinks::getInstance();
+
+        $iLevel = (int)$aParams['level'];
+        $iSelected = (int)$aParams['selected'];
+        $iStart = isset($aParams['start']) ? (int)$aParams['start'] : 0;
+        $iPerPage = isset($aParams['per_page']) ? (int)$aParams['per_page'] : 0;
+
+        $aNodes = $this->_oDb->getContentStructure([
+            'sample' => 'entry_id_full', 
+            'entry_id' => $iContentId, 
+            'level' => $iLevel, 
+            'status' => 'active',
+            'start' => $iStart, 
+            'per_page' => $iPerPage ? $iPerPage + 1 : 0
+        ]);
+
+        if(empty($aNodes) || !is_array($aNodes))
+            return '';
+
+        $aTmplVarsNodes = [];
+        foreach($aNodes as $iKey => $aNode)
+            $aTmplVarsNodes[] = $this->parseHtmlByName('node_l' . $iLevel . '.html', array_merge($aNode, [
+                'bx_if:selected' => [
+                    'condition' => $aNode['node_id'] == $iSelected,
+                    'content' => [true]
+                ],
+                'index' => $iKey,
+                'link' => BX_DOL_URL_ROOT . $oPermalink->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY'] . '&' . $CNF['FIELD_ID'] . '=' . $iContentId, [
+                    'parent_id' => $aNode['node_id']
+                ]),
+                'status' => $this->_getNodeStatus($iProfileId, $iContentId, $aNode['node_id']),
+            ]));
+
+        $oPaginate = new BxTemplPaginate([
+            'start' => $iStart,
+            'per_page' => $iPerPage,
+            'on_change_page' => "return !loadDynamicBlockAutoPaginate(this, '{start}', '{per_page}')"
+        ]);
+        $oPaginate->setNumFromDataArray($aTmplVarsNodes);
+
+        return $this->parseHtmlByName('nodes_l' . $iLevel . '.html', [
+            'level' => $iLevel,
+            'bx_repeat:nodes' => $aTmplVarsNodes,
+            'paginate' => $oPaginate->getSimplePaginate()
+        ]);
+    }
+
+    /**
+     * For 2 levels bases structure ( Max Level = 2)
+     */
+    public function entryStructureByParentMl2($aContentInfo, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($aParams['parent_id']))
+            return '';
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $iParentId = (int)$aParams['parent_id'];
+        $iProfileId = bx_get_logged_profile_id();
+
+        $aNodes = $this->_oDb->getContentStructure([
+            'sample' => 'entry_id_full', 
+            'entry_id' => $iContentId, 
+            'parent_id' => $iParentId,
+            'status' => 'active'
+        ]);
+
+        if(empty($aNodes) || !is_array($aNodes))
+            return '';
+
+        $oPermalink = BxDolPermalinks::getInstance();
+
+        $aTmplVarsNodes = [];
+        foreach($aNodes as $iKey => $aNode) {
+            $aTmplVarsNodes[] = [
+                'node' => $this->parseHtmlByName('ml2_node_l' . $aNode['level'] . '.html', array_merge($aNode, [
+                    'index' => $iKey,
+                    'link' => BX_DOL_URL_ROOT . $oPermalink->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY_NODE'] . '&id=' . $aNode['id']),
+                    'status' => $this->_getNodeStatus($iProfileId, $iContentId, $aNode['node_id']),
+                ]))
+            ];
+        }
+
+        return $this->parseHtmlByName('ml2_nodes_l' . $aSubNode['level'] . '.html', [
+            'bx_repeat:nodes' => $aTmplVarsNodes
+        ]);
+    }
+
+    /**
+     * For 3 levels bases structure ( Max Level = 3)
+     */
+    public function entryStructureByParentMl3($aContentInfo, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(empty($aParams['parent_id']))
+            return '';
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $iParentId = (int)$aParams['parent_id'];
+        $iProfileId = bx_get_logged_profile_id();
+
+        $aNodes = $this->_oDb->getContentStructure([
+            'sample' => 'entry_id_full', 
+            'entry_id' => $iContentId, 
+            'parent_id' => $iParentId,
+            'status' => 'active'
+        ]);
+
+        if(empty($aNodes) || !is_array($aNodes))
+            return '';
+
+        $sJsObject = $this->_oConfig->getJsObject('entry');
+        $oPermalink = BxDolPermalinks::getInstance();
+
+        $aInputs = [];
+        foreach($aNodes as $aNode) {
+            $aInputs['node_' . $aNode['node_id']] = [
+                'type' => 'block_header',
+                'caption' => bx_process_output($aNode['title']),
+                'collapsed' => false,
+                'attrs' => ['id' => 'node_' . $aNode['node_id'], 'class' => ''],
             ];
 
-        return $this->parseHtmlByName('counters.html', [
-            'bx_repeat:counters' => $aTmplVarsCountes
+            $aSubNodes = $this->_oDb->getContentStructure([
+                'sample' => 'entry_id_full', 
+                'entry_id' => $iContentId, 
+                'parent_id' => (int)$aNode['node_id'], 
+                'status' => 'active'
+            ]);
+
+            if(!empty($aSubNodes) && is_array($aSubNodes)) {
+                $aTmplVarsNodes = [];
+                foreach($aSubNodes as $iKey => $aSubNode) {
+                    list($iPassPercent, $sPassProgress, $sPassStatus, $sPassTitle) = $this->_getNodePass($iProfileId, $iContentId, $aSubNode);
+
+                    $sLink = BX_DOL_URL_ROOT . $oPermalink->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY_NODE'] . '&id=' . $iContentId, [
+                        'node_id' => $aSubNode['node_id']
+                    ]);
+
+                    $aTmplVarsNodes[] = [
+                        'node' => $this->parseHtmlByName('ml3_node_l' . $aSubNode['level'] . '.html', array_merge($aSubNode, [
+                            'index' => $iKey,
+                            'link' => $sLink,
+                            'pass_percent' => $iPassPercent,
+                            'pass_progress' => $sPassProgress,
+                            'pass_status' => $sPassStatus,
+                            'bx_if:show_pass' => [
+                                'condition' => $sPassTitle,
+                                'content' => [
+                                    'js_object' => $sJsObject,
+                                    'id' => $aSubNode['node_id'],
+                                    'pass_href' => $sLink,
+                                    'pass_title' => $sPassTitle,
+                                ]
+                            ]
+                        ]))
+                    ];
+                }
+
+                $sInput = 'node_' . $aNode['node_id'] . '_subnodes';
+                $aInputs[$sInput] = [
+                    'type' => 'custom',
+                    'name' => $sInput,
+                    'caption' => '',
+                    'content' => $this->parseHtmlByName('ml3_nodes_l' . $aSubNode['level'] . '.html', [
+                        'bx_repeat:nodes' => $aTmplVarsNodes
+                    ]),
+                ];
+            }
+            else
+                $aInputs['node_' . $aNode['node_id']]['collapsed'] = true;
+        }
+
+        $oForm = new BxTemplFormView([
+            'form_attrs' => [
+                'id' => 'bx-courses-structure-by-parent-' . $iParentId,
+            ],
+            'inputs' => $aInputs,
+        ]);
+        $oForm->setShowEmptySections(true);
+
+        return $oForm->getCode();
+    }
+
+    public function entryNode($aContentInfo, $aParams = [])
+    {
+        $CNF = &$this->_oConfig->CNF;
+
+        if(!isset($aParams['node_id']))
+            return '';
+        
+        $sJsObject = $this->_oConfig->getJsObject('entry');
+
+        $iContentId = (int)$aContentInfo[$CNF['FIELD_ID']];
+        $iNodeId = (int)$aParams['node_id'];
+        $iUsage = isset($aParams['usage']) && $aParams['usage'] !== false ? (int)$aParams['usage'] : BX_COURSES_CND_USAGE_ST;
+        $iProfileId = bx_get_logged_profile_id();
+
+        $aNode = $this->_oDb->getContentNodes([
+            'sample' => 'id_full', 
+            'id' => $iNodeId,
         ]);
+
+        $aDataItems = $this->_oDb->getContentData([
+            'sample' => 'entry_node_ids', 
+            'entry_id' => $iContentId,
+            'node_id' => $iNodeId,
+            'usage' => $iUsage
+        ]);
+
+        $sTxtUndefined = _t('_undefined');
+
+        $aTmplVarsItems = [];
+        if(!empty($aDataItems) && is_array($aDataItems))
+            foreach($aDataItems as $iIndex => $aDataItem) {
+                $sImageUrl = '';
+                if(($sMethod = 'get_thumb') && bx_is_srv($aDataItem['content_type'], $sMethod))
+                    $sImageUrl = bx_srv($aDataItem['content_type'], $sMethod, [$aDataItem['content_id']]);
+
+                $sLink = '';
+                if(($sMethod = 'get_link') && bx_is_srv($aDataItem['content_type'], $sMethod))
+                    $sLink = bx_srv($aDataItem['content_type'], $sMethod, [$aDataItem['content_id']]);
+
+                $aTmplVarsPass = [true];
+                $bTmplVarsPass = $iUsage == BX_COURSES_CND_USAGE_ST && $sLink && !$this->_oModule->isDataPassed($iProfileId, $aDataItem) && ((int)$aNode['passing'] == 0 || $iIndex == 0 || $this->_oModule->isDataPassed($iProfileId, $aDataItems[$iIndex - 1]));
+                if($bTmplVarsPass) {
+                    $aTmplVarsPass = [
+                        'js_object' => $sJsObject,
+                        'id' => $aDataItem['id'],
+                        'link' => $sLink
+                    ];
+                }
+
+                $sType = _t('_bx_courses_txt_data_type_' . $aDataItem['content_type']);
+
+                $sTitle = '';
+                if(($sMethod = 'get_title') && bx_is_srv($aDataItem['content_type'], $sMethod))              
+                    $sTitle = bx_srv($aDataItem['content_type'], $sMethod, [$aDataItem['content_id']]);
+                if(!$sTitle && ($sMethod = 'get_text') &&  bx_is_srv($aDataItem['content_type'], $sMethod))
+                    $sTitle = bx_srv($aDataItem['content_type'], $sMethod, [$aDataItem['content_id']]);
+                if(!$sTitle)
+                    $sTitle = $sTxtUndefined;
+
+                $bTmplVarsShowLink = $iUsage == BX_COURSES_CND_USAGE_AT && $sLink;
+                $aTmplVarsShowLink = $bTmplVarsShowLink ? [
+                    'link' => $sLink,
+                    'title' => $sTitle
+                ] : [true];
+
+                $aTmplVarsItems[] = [
+                    'bx_if:show_image' => [
+                        'condition' => $sImageUrl,
+                        'content' => [
+                            'image' => $sImageUrl
+                        ]
+                    ],
+                    'bx_if:show_image_empty' => [
+                        'condition' => !$sImageUrl,
+                        'content' => [
+                            'type' => $sType
+                        ]
+                    ],
+                    'type' => $sType,
+                    'bx_if:show_link' => [
+                        'condition' => $bTmplVarsShowLink,
+                        'content' => $aTmplVarsShowLink
+                    ],
+                    'bx_if:show_text' => [
+                        'condition' => !$bTmplVarsShowLink,
+                        'content' => [
+                            'title' => $sTitle,
+                        ]
+                    ],
+                    'bx_if:show_pass' => [
+                        'condition' => $bTmplVarsPass,
+                        'content' => $aTmplVarsPass
+                    ]
+                ];
+            }
+
+        $sMiName = 'node-data-';
+        $sMiLink = BX_DOL_URL_ROOT . BxDolPermalinks::getInstance()->permalink('page.php?i=' . $CNF['URI_VIEW_ENTRY_NODE'] . '&id=' . $iContentId, [
+            'node_id' => $iNodeId,
+            'usage' => ''
+        ]);
+
+        $oMenu = new BxTemplMenu([
+            'template' => 'menu_block_submenu_ver.html', 
+            'menu_id'=> 'node-data', 
+            'menu_items' => [
+                ['id' => $sMiName . 'st', 'name' => $sMiName . 'st', 'class' => '', 'link' => $sMiLink . '0', 'target' => '_self', 'title' => _t('_bx_courses_menu_item_title_node_data_steps')],
+                ['id' => $sMiName . 'at', 'name' => $sMiName . 'at', 'class' => '', 'link' => $sMiLink . '1', 'target' => '_self', 'title' => _t('_bx_courses_menu_item_title_node_data_attachments')]
+            ]
+        ]);
+        $oMenu->setSelected('', $sMiName . $this->_oConfig->getUsageI2S($iUsage));
+
+        return [
+            'content' => $this->parseHtmlByName('node_view.html', [
+                'index' => $aNode['order'],
+                'sample' => _t('_bx_courses_txt_sample_l' . $aNode['level'] . '_single'),
+                'title' => $aNode['title'],
+                'text' => $aNode['text'],
+                'bx_repeat:items' => $aTmplVarsItems
+            ]),
+            'menu' => $oMenu
+        ];
+    }
+
+    protected function _getNodeStatus($iProfileId, $iContentId, $iNodeId)
+    {
+        if($this->_oModule->isNodePassed($iProfileId, $iNodeId))
+            return _t('_bx_courses_txt_status_completed');
+
+        if($this->_oModule->isNodeStarted($iProfileId, $iNodeId))
+            return _t('_bx_courses_txt_status_in_process');
+        
+        return _t('_bx_courses_txt_status_not_started');
+    }
+
+    protected function _getNodePass($iProfileId, $iContentId, $aNode)
+    {
+        $iTotal = 0;
+        $iPassCount = $iPassPercent = 0;
+        $sPassStatus = $sPassTitle = '';
+        if(($iTotal = $this->_oModule->getDataTotalByNode($aNode)) != 0) {
+            $aUserTrack = $this->_oDb->getContentData([
+                'sample' => 'user_track', 
+                'entry_id' => $iContentId, 
+                'node_id' => $aNode['node_id'], 
+                'profile_id' => $iProfileId
+            ]);
+
+            $iPassCount = count($aUserTrack);
+            $iPassPercent = round(100 * $iPassCount/$iTotal);
+            
+            if($iPassCount == 0) {
+                $sPassStatus = '_bx_courses_txt_status_not_started';
+                $sPassTitle = '_bx_courses_txt_pass_start';
+            }
+            else {
+                if($iPassCount != $iTotal) {
+                    $sPassStatus = '_bx_courses_txt_status_in_process';
+                    $sPassTitle = '_bx_courses_txt_pass_continue';
+                }
+                else {
+                    $sPassStatus = '_bx_courses_txt_status_completed';
+                    $sPassTitle = '_bx_courses_txt_pass_again';
+                }
+            }
+        }
+
+        return [
+            $iPassPercent,
+            _t('_bx_courses_txt_n_m_steps', $iPassCount, $iTotal),
+            _t($sPassStatus ? $sPassStatus : '_undefined'),
+            _t($sPassTitle)
+        ];
     }
 }
 

+ 48 - 7
modules/boonex/courses/install/langs/en.xml

@@ -4,9 +4,10 @@
     <string name="_bx_courses_cmts"><![CDATA[Courses' Comments]]></string>
 
     <string name="_bx_courses_option_enable_auto_approve"><![CDATA[Auto approve a course after creation]]></string>
-    <string name="_bx_courses_option_enable_content"><![CDATA[Enable possibility to add content in a course]]></string>
+    <string name="_bx_courses_option_enable_content"><![CDATA[Enable possibility to add content in a course (experimental)]]></string>
     <string name="_bx_courses_option_content_level_max"><![CDATA[Max level for a node in tree based structure]]></string>
-    <string name="_bx_courses_option_content_modules"><![CDATA[Modules from which the content can be posted in a course]]></string>
+    <string name="_bx_courses_option_content_modules_step"><![CDATA[Modules from which the content can be added in a course as step]]></string>
+    <string name="_bx_courses_option_content_modules_attachment"><![CDATA[Modules from which the content can be posted in a course as attachment]]></string>
     <string name="_bx_courses_option_num_connections_quick"><![CDATA[Number of items in friends block]]></string>
     <string name="_bx_courses_option_per_page_browse"><![CDATA[Number of items per page]]></string>
     <string name="_bx_courses_option_num_rss"><![CDATA[Number of items in RSS feed]]></string>
@@ -84,6 +85,12 @@
     <string name="_bx_courses_form_content_node_input_sys_title"><![CDATA[Title]]></string>
     <string name="_bx_courses_form_content_node_input_title"><![CDATA[Title]]></string>
     <string name="_bx_courses_form_content_node_input_title_err"><![CDATA[Title is essential. Please, fill in this field.]]></string>
+    <string name="_bx_courses_form_content_node_input_sys_text"><![CDATA[Description]]></string>
+    <string name="_bx_courses_form_content_node_input_text"><![CDATA[Description]]></string>
+    <string name="_bx_courses_form_content_node_input_sys_passing"><![CDATA[Passing]]></string>
+    <string name="_bx_courses_form_content_node_input_passing"><![CDATA[Passing]]></string>
+    <string name="_bx_courses_form_content_node_input_passing_all"><![CDATA[All at once]]></string>
+    <string name="_bx_courses_form_content_node_input_passing_alternate"><![CDATA[Alternate]]></string>
     <string name="_bx_courses_form_content_node_input_sys_do_submit"><![CDATA[Submit]]></string>
     <string name="_bx_courses_form_content_node_input_do_submit"><![CDATA[Submit]]></string>
     <string name="_bx_courses_form_content_node_input_sys_do_cancel"><![CDATA[Cancel]]></string>
@@ -111,7 +118,9 @@
     <string name="_bx_courses_page_title_join_profile"><![CDATA[Join Course]]></string>
     <string name="_bx_courses_page_title_sys_view_profile_closed"><![CDATA[View Closed Course]]></string>
     <string name="_bx_courses_page_title_sys_view_profile"><![CDATA[View Course]]></string>
+    <string name="_bx_courses_page_title_sys_view_profile_node"><![CDATA[View Lesson]]></string>
     <string name="_bx_courses_page_title_view_profile"><![CDATA[{display_name}]]></string>
+    <string name="_bx_courses_page_title_view_profile_node"><![CDATA[Lesson]]></string>
     <string name="_bx_courses_page_title_browse"><![CDATA[Courses]]></string>
     <string name="_bx_courses_page_title_browse_by_author"><![CDATA[Courses of {display_name}]]></string>
     <string name="_bx_courses_page_title_browse_featured"><![CDATA[Featured Courses]]></string>
@@ -176,6 +185,8 @@
     <string name="_bx_courses_page_block_title_system_profile_data"><![CDATA[Content]]></string>
     <string name="_bx_courses_page_block_title_profile_data_link"><![CDATA[Content of <a href="{profile_link}">{display_name}</a>]]></string>
     <string name="_bx_courses_page_block_title_profile_description"><![CDATA[Description]]></string>
+    <string name="_bx_courses_page_block_title_profile_structure_l1"><![CDATA[Modules]]></string>
+    <string name="_bx_courses_page_block_title_profile_structure_l2"><![CDATA[Themes]]></string>
     <string name="_bx_courses_page_block_title_profile_friends_link"><![CDATA[Friends of <a href="{profile_link}">{display_name}</a>]]></string>
     <string name="_bx_courses_page_block_title_profile_friends"><![CDATA[Friends]]></string>
     <string name="_bx_courses_page_block_title_system_profile_friends"><![CDATA[Friends]]></string>
@@ -198,6 +209,8 @@
     <string name="_bx_courses_page_block_title_joined_profiles"><![CDATA[Attended Courses]]></string>
     <string name="_bx_courses_page_block_title_entry_all_actions"><![CDATA[All Actions]]></string>
     <string name="_bx_courses_page_block_title_entry_social_sharing"><![CDATA[Share]]></string>
+    <string name="_bx_courses_page_block_title_system_entry_node"><![CDATA[Lesson]]></string>
+    <string name="_bx_courses_page_block_title_entry_node"><![CDATA[]]></string>
     <string name="_bx_courses_page_block_title_categories"><![CDATA[Courses Categories]]></string>
     <string name="_bx_courses_page_block_title_invite_to_course"><![CDATA[Invite Students to the Course]]></string>
     <string name="_bx_courses_page_block_title_search_form"><![CDATA[Search Form]]></string>
@@ -332,6 +345,11 @@
     <string name="_bx_courses_menu_item_title_sm_leave_reject"><![CDATA[Reject attend request]]></string>
     <string name="_bx_courses_menu_item_title_sm_leave_cancel"><![CDATA[Cancel attend request]]></string>
 
+    <string name="_bx_courses_menu_title_content_add"><![CDATA[Add Content]]></string>
+    <string name="_bx_courses_menu_set_title_content_add"><![CDATA[Add Content]]></string>
+    <string name="_bx_courses_menu_item_title_node_data_steps"><![CDATA[Steps]]></string>
+    <string name="_bx_courses_menu_item_title_node_data_attachments"><![CDATA[Attachments]]></string>
+
     <string name="_bx_courses_grid_filter_item_title_adm_select_one_filter1"><![CDATA[All Statuses]]></string>
     <string name="_bx_courses_grid_filter_item_title_adm_select_one_filter2"><![CDATA[Any Audience]]></string>
     <string name="_bx_courses_grid_filter_item_title_adm_active"><![CDATA[Active]]></string>
@@ -372,12 +390,14 @@
     <string name="_bx_courses_grid_column_title_cnt_cn_l2"><![CDATA[Themes]]></string>
     <string name="_bx_courses_grid_column_title_cnt_cn_l3"><![CDATA[Lessons]]></string>
     <string name="_bx_courses_grid_column_title_cnt_counters"><![CDATA[Info]]></string>
-    <string name="_bx_courses_grid_column_title_cnt_added"><![CDATA[Added]]></string>
     <string name="_bx_courses_grid_column_title_cnt_content_id"><![CDATA[Content]]></string>
     <string name="_bx_courses_grid_column_title_cnt_content_type"><![CDATA[Type]]></string>
+    <string name="_bx_courses_grid_column_title_cnt_usage"><![CDATA[Usage]]></string>
+    <string name="_bx_courses_grid_column_title_cnt_added"><![CDATA[Added]]></string>
     <string name="_bx_courses_grid_action_title_cnt_back"><![CDATA[Back]]></string>
     <string name="_bx_courses_grid_action_title_cnt_add"><![CDATA[Add {node}]]></string>
-    <string name="_bx_courses_grid_action_title_cnt_add_content"><![CDATA[Add]]></string>
+    <string name="_bx_courses_grid_action_title_cnt_add_step"><![CDATA[Add step]]></string>
+    <string name="_bx_courses_grid_action_title_cnt_add_attachment"><![CDATA[Add attachment]]></string>
     <string name="_bx_courses_grid_action_title_cnt_edit"><![CDATA[Edit]]></string>
     <string name="_bx_courses_grid_action_title_cnt_delete"><![CDATA[Delete]]></string>
     <string name="_bx_courses_popup_title_cnt_add"><![CDATA[Add {node}]]></string>
@@ -413,9 +433,30 @@
     <string name="_bx_courses_txt_n_unit"><![CDATA[{0} ({1})]]></string>
     <string name="_bx_courses_txt_cart_item_title"><![CDATA[{0}: {1} ({2} {3})]]></string>
     <string name="_bx_courses_txt_cart_item_title_lifetime"><![CDATA[{0}: {1} (lifetime)]]></string>    
-    <string name="_bx_courses_txt_module"><![CDATA[module]]></string>
-    <string name="_bx_courses_txt_theme"><![CDATA[theme]]></string>
-    <string name="_bx_courses_txt_lesson"><![CDATA[lesson]]></string>
+    <string name="_bx_courses_txt_sample_l1_single"><![CDATA[module]]></string>
+    <string name="_bx_courses_txt_sample_l1_plural"><![CDATA[modules]]></string>
+    <string name="_bx_courses_txt_sample_l2_single"><![CDATA[theme]]></string>
+    <string name="_bx_courses_txt_sample_l2_plural"><![CDATA[themes]]></string>
+    <string name="_bx_courses_txt_sample_l3_single"><![CDATA[lesson]]></string>
+    <string name="_bx_courses_txt_sample_l3_plural"><![CDATA[lessons]]></string>
+    <string name="_bx_courses_txt_status_not_started"><![CDATA[not started]]></string>
+    <string name="_bx_courses_txt_status_in_process"><![CDATA[in process]]></string>
+    <string name="_bx_courses_txt_status_completed"><![CDATA[completed]]></string>
+    <string name="_bx_courses_txt_pass_start"><![CDATA[Start]]></string>
+    <string name="_bx_courses_txt_pass_continue"><![CDATA[Continue]]></string>
+    <string name="_bx_courses_txt_pass_again"><![CDATA[Pass again]]></string>
+    <string name="_bx_courses_txt_pass"><![CDATA[Pass]]></string>
+    <string name="_bx_courses_txt_steps"><![CDATA[steps]]></string>
+    <string name="_bx_courses_txt_n_m_steps"><![CDATA[{0} / {1} steps]]></string>
+    <string name="_bx_courses_txt_attachments"><![CDATA[attachments]]></string>
+    <string name="_bx_courses_txt_data_type_bx_files"><![CDATA[file]]></string>
+    <string name="_bx_courses_txt_data_type_bx_photos"><![CDATA[image]]></string>
+    <string name="_bx_courses_txt_data_type_bx_polls"><![CDATA[poll]]></string>
+    <string name="_bx_courses_txt_data_type_bx_posts"><![CDATA[theory]]></string>
+    <string name="_bx_courses_txt_data_type_bx_videos"><![CDATA[video]]></string>
+    <string name="_bx_courses_txt_data_usage_0"><![CDATA[step]]></string>
+    <string name="_bx_courses_txt_data_usage_1"><![CDATA[attachment]]></string>
+    
 
     <string name="_bx_courses_msg_performed"><![CDATA[The action was successfully performed.]]></string>
     <string name="_bx_courses_err_price_duplicate"><![CDATA[Similar pricing plan is already exists.]]></string>

+ 1 - 1
modules/boonex/courses/install/sql/disable.sql

@@ -8,7 +8,7 @@ DELETE FROM `sys_options_types` WHERE `id` = @iTypeId;
 
 -- PAGES
 DELETE FROM `sys_objects_page` WHERE `module` = 'bx_courses';
-DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_courses' OR `object` IN('bx_courses_create_profile', 'bx_courses_delete_profile', 'bx_courses_join_profile', 'bx_courses_edit_profile', 'bx_courses_edit_profile_cover', 'bx_courses_invite', 'bx_courses_view_profile', 'bx_courses_view_profile_closed', 'bx_courses_profile_info', 'bx_courses_profile_pricing', 'bx_courses_profile_comments', 'bx_courses_home', 'bx_courses_fans', 'bx_courses_joined', 'bx_courses_favorites', 'bx_courses_top', 'bx_courses_search', 'bx_courses_manage', 'bx_courses_administration');
+DELETE FROM `sys_pages_blocks` WHERE `module` = 'bx_courses' OR `object` IN('bx_courses_create_profile', 'bx_courses_delete_profile', 'bx_courses_join_profile', 'bx_courses_edit_profile', 'bx_courses_edit_profile_cover', 'bx_courses_invite', 'bx_courses_view_profile', 'bx_courses_view_profile_closed', 'bx_courses_view_profile_node', 'bx_courses_profile_info', 'bx_courses_profile_pricing', 'bx_courses_profile_comments', 'bx_courses_home', 'bx_courses_fans', 'bx_courses_joined', 'bx_courses_favorites', 'bx_courses_top', 'bx_courses_search', 'bx_courses_manage', 'bx_courses_administration');
 
 -- MENU
 DELETE FROM `sys_objects_menu` WHERE `module` = 'bx_courses';

+ 19 - 7
modules/boonex/courses/install/sql/enable.sql

@@ -14,7 +14,8 @@ INSERT INTO `sys_options` (`name`, `value`, `category_id`, `caption`, `type`, `e
 ('bx_courses_enable_auto_approve', 'on', @iCategId, '_bx_courses_option_enable_auto_approve', 'checkbox', '', '', '', 0),
 ('bx_courses_enable_content', '', @iCategId, '_bx_courses_option_enable_content', 'checkbox', '', '', '', 5),
 ('bx_courses_content_level_max', '3', @iCategId, '_bx_courses_option_content_level_max', 'select', '1,2,3,4,5', '', '', 6),
-('bx_courses_content_modules', '', @iCategId, '_bx_courses_option_content_modules', 'list', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:27:"get_options_content_modules";}', '', '', 7), 
+('bx_courses_content_modules_st', '', @iCategId, '_bx_courses_option_content_modules_step', 'list', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:30:"get_options_content_modules_st";}', '', '', 7),
+('bx_courses_content_modules_at', '', @iCategId, '_bx_courses_option_content_modules_attachment', 'list', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:30:"get_options_content_modules_at";}', '', '', 8),
 ('bx_courses_num_connections_quick', '6', @iCategId, '_bx_courses_option_num_connections_quick', 'digit', '', '', '', 10),
 ('bx_courses_per_page_browse', '24', @iCategId, '_bx_courses_option_per_page_browse', 'digit', '', '', '', 11),
 ('bx_courses_num_rss', '10', @iCategId, '_bx_courses_option_num_rss', 'digit', '', '', '', 12),
@@ -44,6 +45,8 @@ INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `t
 ('bx_courses_view_profile', 1, 'bx_courses', '', '_bx_courses_page_block_title_entry_social_sharing', 13, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:21:"entity_social_sharing";}', 0, 0, 0, 0),
 ('bx_courses_view_profile', 1, 'bx_courses', '', '_bx_courses_page_block_title_entry_all_actions', 13, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:18:"entity_all_actions";}', 0, 0, 0, 0),
 ('bx_courses_view_profile', 2, 'bx_courses', '', '_bx_courses_page_block_title_profile_description', 13, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:17:"entity_text_block";}', 0, 0, 1, 0),
+('bx_courses_view_profile', 2, 'bx_courses', '', '_bx_courses_page_block_title_profile_structure_l1', 13, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:25:"entity_structure_l1_block";}', 0, 0, 1, 1),
+('bx_courses_view_profile', 2, 'bx_courses', '', '_bx_courses_page_block_title_profile_structure_l2', 13, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:25:"entity_structure_l2_block";}', 0, 0, 1, 2),
 ('bx_courses_view_profile', 3, 'bx_courses', '', '_bx_courses_page_block_title_profile_location', 13, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:15:"entity_location";}', 0, 0, 0, 0),
 ('bx_courses_view_profile', 3, 'bx_courses', '', '_bx_courses_page_block_title_profile_info', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:11:"entity_info";}', 0, 0, 1, 0),
 ('bx_courses_view_profile', 3, 'bx_courses', '', '_bx_courses_page_block_title_admins', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:6:"admins";}', 0, 0, 1, 1),
@@ -52,6 +55,13 @@ INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `t
 ('bx_courses_view_profile', 4, 'bx_courses', '', '_bx_courses_page_block_title_profile_comments', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:15:"entity_comments";}', 0, 0, 0, 0),
 ('bx_courses_view_profile', 2, 'bx_courses', '', '_bx_courses_page_block_title_entry_reports', 11, 2147483647, 'service', 'a:2:{s:6:\"module\";s:10:\"bx_courses\";s:6:\"method\";s:14:\"entity_reports\";}', 0, 0, 1, 6);
 
+-- PAGE: view profile node 
+
+INSERT INTO `sys_objects_page`(`object`, `uri`, `title_system`, `title`, `module`, `layout_id`, `visible_for_levels`, `visible_for_levels_editable`, `url`, `meta_description`, `meta_keywords`, `meta_robots`, `cache_lifetime`, `cache_editable`, `deletable`, `override_class_name`, `override_class_file`) VALUES 
+('bx_courses_view_profile_node', 'view-course-profile-node', '_bx_courses_page_title_sys_view_profile_node', '_bx_courses_page_title_view_profile_node', 'bx_courses', 5, 2147483647, 1, 'page.php?i=view-course-profile-node', '', '', '', 0, 1, 0, 'BxCoursesPageEntry', 'modules/boonex/courses/classes/BxCoursesPageEntry.php');
+
+INSERT INTO `sys_pages_blocks`(`object`, `cell_id`, `module`, `title_system`, `title`, `designbox_id`, `visible_for_levels`, `type`, `content`, `deletable`, `copyable`, `active`, `order`) VALUES
+('bx_courses_view_profile_node', 1, 'bx_courses', '_bx_courses_page_block_title_system_entry_node', '_bx_courses_page_block_title_entry_node', 11, 2147483647, 'service', 'a:2:{s:6:"module";s:10:"bx_courses";s:6:"method";s:17:"entity_node_block";}', 0, 0, 1, 0);
 
 -- PAGE: view closed profile 
 
@@ -316,10 +326,10 @@ INSERT INTO `sys_menu_items`(`set_name`, `module`, `name`, `title_system`, `titl
 -- MENU: add content menu for entry
 
 INSERT INTO `sys_objects_menu`(`object`, `title`, `set_name`, `module`, `template_id`, `persistent`, `deletable`, `active`, `override_class_name`, `override_class_file`) VALUES 
-('bx_courses_content_add', '_sys_menu_title_content_add', 'bx_courses_content_add', 'bx_courses', 6, 1, 0, 1, 'BxCoursesMenuContentAdd', 'modules/boonex/courses/classes/BxCoursesMenuContentAdd.php');
+('bx_courses_content_add', '_bx_courses_menu_title_content_add', 'bx_courses_content_add', 'bx_courses', 6, 1, 0, 1, 'BxCoursesMenuContentAdd', 'modules/boonex/courses/classes/BxCoursesMenuContentAdd.php');
 
 INSERT INTO `sys_menu_sets`(`set_name`, `module`, `title`, `deletable`) VALUES 
-('bx_courses_content_add', 'bx_courses', '_sys_menu_set_title_content_add', 0);
+('bx_courses_content_add', 'bx_courses', '_bx_courses_menu_set_title_content_add', 0);
 
 -- MENU: actions menu for my entries
 
@@ -654,14 +664,16 @@ INSERT INTO `sys_objects_grid` (`object`, `source_type`, `source`, `table`, `fie
 INSERT INTO `sys_grid_fields` (`object`, `name`, `title`, `width`, `translatable`, `chars_limit`, `params`, `order`) VALUES
 ('bx_courses_cnt_data_manage', 'order', '', '1%', 0, 0, '', 1),
 ('bx_courses_cnt_data_manage', 'checkbox', '_sys_select', '2%', 0, 0, '', 2),
-('bx_courses_cnt_data_manage', 'content_id', '_bx_courses_grid_column_title_cnt_content_id', '47%', 0, 0, '', 3),
+('bx_courses_cnt_data_manage', 'content_id', '_bx_courses_grid_column_title_cnt_content_id', '42%', 0, 0, '', 3),
 ('bx_courses_cnt_data_manage', 'content_type', '_bx_courses_grid_column_title_cnt_content_type', '15%', 0, 0, '', 4),
-('bx_courses_cnt_data_manage', 'added', '_bx_courses_grid_column_title_cnt_added', '15%', 0, 0, '', 5),
-('bx_courses_cnt_data_manage', 'actions', '', '20%', 0, '', '', 6);
+('bx_courses_cnt_data_manage', 'usage', '_bx_courses_grid_column_title_cnt_usage', '10%', 0, 0, '', 5),
+('bx_courses_cnt_data_manage', 'added', '_bx_courses_grid_column_title_cnt_added', '10%', 0, 0, '', 6),
+('bx_courses_cnt_data_manage', 'actions', '', '20%', 0, '', '', 7);
 
 INSERT INTO `sys_grid_actions` (`object`, `type`, `name`, `title`, `icon`, `icon_only`, `confirm`, `order`) VALUES
 ('bx_courses_cnt_data_manage', 'independent', 'back', '_bx_courses_grid_action_title_cnt_back', 'arrow-left', 0, 0, 1),
-('bx_courses_cnt_data_manage', 'independent', 'add', '_bx_courses_grid_action_title_cnt_add_content', '', 0, 0, 2),
+('bx_courses_cnt_data_manage', 'independent', 'add_st', '_bx_courses_grid_action_title_cnt_add_step', '', 0, 0, 2),
+('bx_courses_cnt_data_manage', 'independent', 'add_at', '_bx_courses_grid_action_title_cnt_add_attachment', '', 0, 0, 3),
 ('bx_courses_cnt_data_manage', 'single', 'delete', '_bx_courses_grid_action_title_cnt_delete', 'remove', 1, 1, 1),
 ('bx_courses_cnt_data_manage', 'bulk', 'delete', '_bx_courses_grid_action_title_cnt_delete', '', 0, 1, 1);
 

+ 33 - 6
modules/boonex/courses/install/sql/install.sql

@@ -51,23 +51,44 @@ CREATE TABLE IF NOT EXISTS `bx_courses_content_nodes` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `entry_id` int(11) NOT NULL default '0',
   `title` varchar(255) NOT NULL DEFAULT '',
+  `text` text NOT NULL,
+  `passing` tinyint(4) NOT NULL DEFAULT '0',
   `counters` text NOT NULL,
   `added` int(11) NOT NULL,
   `status` enum('active','hidden') NOT NULL DEFAULT 'active',
   PRIMARY KEY (`id`)
 );
 
+CREATE TABLE IF NOT EXISTS `bx_courses_content_nodes2users` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `node_id` int(11) NOT NULL default '0',
+  `profile_id` int(11) NOT NULL default '0',
+  `date` int(11) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `pass` (`node_id`, `profile_id`)
+);
+
 CREATE TABLE IF NOT EXISTS `bx_courses_content_data` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `entry_id` int(11) NOT NULL default '0',
   `node_id` int(11) NOT NULL DEFAULT '0',
   `content_type` varchar(32) NOT NULL DEFAULT '',
   `content_id` int(11) NOT NULL DEFAULT '0',
+  `usage` tinyint(4) NOT NULL DEFAULT '0',
   `added` int(11) NOT NULL DEFAULT '0',
   `order` int(11) NOT NULL DEFAULT '0',
   PRIMARY KEY (`id`)
 );
 
+CREATE TABLE IF NOT EXISTS `bx_courses_content_data2users` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `data_id` int(11) NOT NULL default '0',
+  `profile_id` int(11) NOT NULL default '0',
+  `date` int(11) NOT NULL DEFAULT '0',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `pass` (`data_id`, `profile_id`)
+);
+
 -- TABLE: STORAGES & TRANSCODERS
 CREATE TABLE IF NOT EXISTS `bx_courses_pics` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
@@ -463,6 +484,8 @@ INSERT INTO `sys_form_displays` (`display_name`, `module`, `object`, `title`, `v
 
 INSERT INTO `sys_form_inputs` (`object`, `module`, `name`, `value`, `values`, `checked`, `type`, `caption_system`, `caption`, `info`, `required`, `collapsed`, `html`, `attrs`, `attrs_tr`, `attrs_wrapper`, `checker_func`, `checker_params`, `checker_error`, `db_pass`, `db_params`, `editable`, `deletable`) VALUES
 ('bx_courses_content_node', 'bx_courses', 'title', '', '', 0, 'text', '_bx_courses_form_content_node_input_sys_title', '_bx_courses_form_content_node_input_title', '', 1, 0, 0, '', '', '', 'Avail', '', '_bx_courses_form_content_node_input_title_err', 'Xss', '', 1, 0),
+('bx_courses_content_node', 'bx_courses', 'text', '', '', 0, 'textarea', '_bx_courses_form_content_node_input_sys_text', '_bx_courses_form_content_node_input_text', '', 0, 0, 2, '', '', '', '', '', '', 'XssHtml', '', 1, 0),
+('bx_courses_content_node', 'bx_courses', 'passing', '', '', 0, 'select', '_bx_courses_form_content_node_input_sys_passing', '_bx_courses_form_content_node_input_passing', '', 0, 0, 0, '', '', '', '', '', '', 'Xss', '', 1, 0),
 ('bx_courses_content_node', 'bx_courses', 'controls', '', 'do_submit,do_cancel', 0, 'input_set', '', '', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
 ('bx_courses_content_node', 'bx_courses', 'do_submit', '_bx_courses_form_content_node_input_do_submit', '', 0, 'submit', '_bx_courses_form_content_node_input_sys_do_submit', '', '', 0, 0, 0, '', '', '', '', '', '', '', '', 1, 0),
 ('bx_courses_content_node', 'bx_courses', 'do_cancel', '_bx_courses_form_content_node_input_do_cancel', '', 0, 'button', '_bx_courses_form_content_node_input_sys_do_cancel', '', '', 0, 0, 0, 'a:2:{s:7:"onclick";s:45:"$(''.bx-popup-applied:visible'').dolPopupHide()";s:5:"class";s:22:"bx-def-margin-sec-left";}', '', '', '', '', '', '', '', 1, 0);
@@ -470,15 +493,19 @@ INSERT INTO `sys_form_inputs` (`object`, `module`, `name`, `value`, `values`, `c
 INSERT INTO `sys_form_display_inputs` (`display_name`, `input_name`, `visible_for_levels`, `active`, `order`) VALUES
 ('bx_courses_content_node_add', 'id', 2147483647, 0, 1),
 ('bx_courses_content_node_add', 'title', 2147483647, 1, 2),
-('bx_courses_content_node_add', 'controls', 2147483647, 1, 3),
-('bx_courses_content_node_add', 'do_submit', 2147483647, 1, 4),
-('bx_courses_content_node_add', 'do_cancel', 2147483647, 1, 5),
+('bx_courses_content_node_add', 'text', 2147483647, 1, 3),
+('bx_courses_content_node_add', 'passing', 2147483647, 1, 4),
+('bx_courses_content_node_add', 'controls', 2147483647, 1, 5),
+('bx_courses_content_node_add', 'do_submit', 2147483647, 1, 6),
+('bx_courses_content_node_add', 'do_cancel', 2147483647, 1, 7),
 
 ('bx_courses_content_node_edit', 'id', 2147483647, 1, 1),
 ('bx_courses_content_node_edit', 'title', 2147483647, 1, 2),
-('bx_courses_content_node_edit', 'controls', 2147483647, 1, 3),
-('bx_courses_content_node_edit', 'do_submit', 2147483647, 1, 4),
-('bx_courses_content_node_edit', 'do_cancel', 2147483647, 1, 5);
+('bx_courses_content_node_edit', 'text', 2147483647, 1, 3),
+('bx_courses_content_node_edit', 'passing', 2147483647, 1, 4),
+('bx_courses_content_node_edit', 'controls', 2147483647, 1, 5),
+('bx_courses_content_node_edit', 'do_submit', 2147483647, 1, 6),
+('bx_courses_content_node_edit', 'do_cancel', 2147483647, 1, 7);
 
 -- PRE-VALUES
 INSERT INTO `sys_form_pre_lists`(`key`, `title`, `module`, `use_for_sets`) VALUES

+ 1 - 1
modules/boonex/courses/install/sql/uninstall.sql

@@ -1,7 +1,7 @@
 
 -- TABLES
 
-DROP TABLE IF EXISTS `bx_courses_data`, `bx_courses_content_structure`, `bx_courses_content_nodes`, `bx_courses_content_data`, `bx_courses_pics`, `bx_courses_pics_resized`, `bx_courses_cmts`, `bx_courses_cmts_notes`, `bx_courses_views_track`, `bx_courses_meta_keywords`, `bx_courses_meta_locations`, `bx_courses_meta_mentions`, `bx_courses_fans`, `bx_courses_admins`, `bx_courses_votes`, `bx_courses_votes_track`, `bx_courses_reports`, `bx_courses_reports_track`, `bx_courses_favorites_track`, `bx_courses_favorites_lists`, `bx_courses_scores`, `bx_courses_scores_track`, `bx_courses_invites`, `bx_courses_prices`;
+DROP TABLE IF EXISTS `bx_courses_data`, `bx_courses_content_structure`, `bx_courses_content_nodes`, `bx_courses_content_nodes2users`, `bx_courses_content_data`, `bx_courses_content_data2users`, `bx_courses_pics`, `bx_courses_pics_resized`, `bx_courses_cmts`, `bx_courses_cmts_notes`, `bx_courses_views_track`, `bx_courses_meta_keywords`, `bx_courses_meta_locations`, `bx_courses_meta_mentions`, `bx_courses_fans`, `bx_courses_admins`, `bx_courses_votes`, `bx_courses_votes_track`, `bx_courses_reports`, `bx_courses_reports_track`, `bx_courses_favorites_track`, `bx_courses_favorites_lists`, `bx_courses_scores`, `bx_courses_scores_track`, `bx_courses_invites`, `bx_courses_prices`;
 
 -- PROFILES
 

+ 66 - 0
modules/boonex/courses/js/entry.js

@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) UNA, Inc - https://una.io
+ * MIT License - https://opensource.org/licenses/MIT
+ *
+ * @defgroup    Courses Courses
+ * @ingroup     UnaModules
+ *
+ * @{
+ */
+
+function BxCoursesEntry(oOptions) {
+    BxBaseModGroupsEntry.call(this, oOptions);
+
+    this._sObjName = oOptions.sObjName == undefined ? 'oBxCoursesEntry' : oOptions.sObjName;
+}
+
+BxCoursesEntry.prototype = Object.create(BxBaseModGroupsEntry.prototype);
+BxCoursesEntry.prototype.constructor = BxCoursesEntry;
+
+BxCoursesEntry.prototype.passNode = function(oElement, iNodeId) {
+    var $this = this;
+    var oParams = this._getDefaultData();
+    oParams['node_id'] = iNodeId;
+
+    if(oElement)
+        this.loadingInButton(oElement, true);
+
+    jQuery.get (
+        this._sActionsUrl + 'pass_node',
+        oParams,
+        function(oData) {
+            if(oElement)
+                $this.loadingInButton(oElement, false);
+
+            processJsonData(oData);
+        },
+        'json'
+    );
+
+    return false;
+};
+
+BxCoursesEntry.prototype.passData = function(oElement, iDataId) {
+    var $this = this;
+    var oParams = this._getDefaultData();
+    oParams['data_id'] = iDataId;
+
+    if(oElement)
+        this.loadingInButton(oElement, true);
+
+    jQuery.get (
+        this._sActionsUrl + 'pass_data',
+        oParams,
+        function(oData) {
+            if(oElement)
+                $this.loadingInButton(oElement, false);
+
+            processJsonData(oData);
+        },
+        'json'
+    );
+
+    return false;
+};
+
+/** @} */

+ 17 - 4
modules/boonex/courses/template/counters.html

@@ -1,5 +1,18 @@
-<div class="bx-courses-counters flex flex-row flex-wrap">
-    <bx_repeat:counters>
-        <div class="m-px px-2 bg-gray-200 border rounded-md whitespace-nowrap">__value__ <span class="lowercase">__title__</span></div>
-    </bx_repeat:counters>
+<div class="bx-courses-counters flex flex-col">
+    <bx_if:show_st>
+        <div class="bx-courses-c-title capitalize"><bx_text:_bx_courses_txt_steps />:</div>
+        <div class="bx-courses-c-list flex flex-row flex-wrap">
+            <bx_repeat:counters_st>
+                <div class="m-px px-2 bg-gray-200 border rounded-md whitespace-nowrap">__value__ <span class="lowercase">__title__</span></div>
+            </bx_repeat:counters_st>
+        </div>
+    </bx_if:show_st>
+    <bx_if:show_at>
+        <div class="bx-courses-c-title mt-2 capitalize"><bx_text:_bx_courses_txt_attachments />:</div>
+        <div class="bx-courses-c-list flex flex-row flex-wrap">
+            <bx_repeat:counters_at>
+                <div class="m-px px-2 bg-gray-200 border rounded-md whitespace-nowrap">__value__ <span class="lowercase">__title__</span></div>
+            </bx_repeat:counters_at>
+        </div>
+    </bx_if:show_at>
 </div>

+ 1 - 0
modules/boonex/courses/template/ml2_node_l2.html

@@ -0,0 +1 @@
+<bx_include_auto:ml3_node_l3.html />

+ 1 - 0
modules/boonex/courses/template/ml2_nodes_l2.html

@@ -0,0 +1 @@
+<bx_include_auto:ml2_node_l2.html />

+ 24 - 0
modules/boonex/courses/template/ml3_node_l3.html

@@ -0,0 +1,24 @@
+<div class="bx-courses-ml3-node-l__level__ bx-def-box bx-def-box-round-corners bx-def-color-bg-box relative my-2">
+    <div class="bx-courses-ml3-node-l__level__-gap p-4">
+        <div class="bx-courses-ml3-node-l__level__-cnt relative flex space-x-4">
+            <div class="bx-courses-ml3-node-l__level__-progress pr-4 border-r">
+                <div class="bx-courses-percent flex justify-center items-center w-16 h-16 border-2 rounded-full">
+                    <span>__pass_percent__%</span>
+                </div>
+                <div class="bx-courses-percent text-center">__pass_progress__</div>
+            </div>
+            <div class="bx-courses-ml3-node-l__level__-info flex-1">
+                <div class="bx-courses-index bx-def-font-grayed text-xs"><span class="capitalize"><bx_text:_bx_courses_txt_sample_l3_single /></span> __index__:</div>
+                <div class="bx-courses-title text-xl font-bold">
+                    <a href="__link__">__title__</a>
+                </div>
+                <div class="bx-courses-status flex pt-2"><div class="px-4 bg-gray-300 rounded-full">__pass_status__</div></div>
+            </div>
+            <div class="bx-courses-ml3-node-l__level__-actions flex items-center">
+                <bx_if:show_pass>
+                    <a class="bx-btn bx-btn-small" href="__pass_href__" onclick="javascript:return __js_object__.passNode(this, __id__);">__pass_title__</a>
+                </bx_if:show_pass>
+            </div>
+        </div>
+    </div>
+</div>

+ 3 - 0
modules/boonex/courses/template/ml3_nodes_l3.html

@@ -0,0 +1,3 @@
+<div class="bx-courses-ml3-nodes-l__level__ flex flex-col">
+    <bx_repeat:nodes>__node__</bx_repeat:nodes>
+</div>

+ 15 - 0
modules/boonex/courses/template/node_l1.html

@@ -0,0 +1,15 @@
+<div class="bx-courses-node-l__level__ bx-def-box bx-def-box-round-corners bx-def-color-bg-box bx-def-color-bg-box-hover relative w-1/3 m-2<bx_if:selected> bg-gray-100</bx_if:selected>">
+    <a class="bx-courses-node-l__level__-link hover:no-underline" href="__link__">
+        <div class="bx-courses-node-l__level__-gap p-4">
+            <div class="bx-courses-node-l__level__-cnt relative flex flex-col">
+                <div class="bx-courses-status flex pb-2"><div class="px-4 bg-gray-300 rounded-full">__status__</div></div>
+                <div class="bx-courses-index bx-def-font-grayed text-xs"><span class="capitalize"><bx_text:_bx_courses_txt_sample_l1_single /></span> __index__:</div>
+                <div class="bx-courses-title font-bold">__title__</div>
+                <div class="bx-courses-counters flex pt-2 space-x-2">
+                    <div><bx_text:_bx_courses_txt_sample_l2_plural />: <span class="font-bold">__cn_l2__</span></div>
+                    <div><bx_text:_bx_courses_txt_sample_l3_plural />: <span class="font-bold">__cn_l3__</span></div>
+                </div>
+            </div>
+        </div>
+    </a>
+</div>

+ 33 - 0
modules/boonex/courses/template/node_view.html

@@ -0,0 +1,33 @@
+<div class="bx-courses-node-view">
+    <div class="bx-courses-node-view-cnt">
+        <div class="bx-courses-index bx-def-font-grayed text-xs"><span class="capitalize">__sample__</span> __index__:</div>
+        <div class="bx-courses-title text-2xl font-bold">__title__</div>
+        <div class="bx-courses-text bx-def-font-grayed text-xs">__text__</div>
+        <div class="bx-courses-node-items">
+            <bx_repeat:items>
+                <div class="bx-courses-node-item flex items-center space-x-4 py-4 border-t first:border-0">
+                    <div class="bx-courses-ni-image">
+                        <div class="bx-courses-nii w-48 h-28 box-content border rounded-lg overflow-hidden">
+                            <bx_if:show_image><img class="block w-48 h-28 object-fill" src="__image__" /></bx_if:show_image>
+                            <bx_if:show_image_empty>
+                                <div class="flex justify-center items-center w-48 h-28 bg-gray-100 text-2xl uppercase">__type__</div>
+                            </bx_if:show_image_empty>
+                        </div>
+                    </div>
+                    <div class="bx-courses-ni-data flex-1">
+                        <div class="bx-courses-nid-type flex"><div class="px-4 bg-gray-300 rounded-full capitalize">__type__</div></div>
+                        <div class="bx-courses-nid-title text-lg font-bold">
+                            <bx_if:show_link><a href="__link__" target="_blank">__title__</a></bx_if:show_link>
+                            <bx_if:show_text>__title__</bx_if:show_text>
+                        </div>
+                    </div>
+                    <div class="bx-courses-ni-action flex items-center">
+                        <bx_if:show_pass>
+                            <a class="bx-btn bx-btn-small" href="__link__" onclick="javascript:return __js_object__.passData(this, __id__);"><bx_text:_bx_courses_txt_pass /></a>
+                        </bx_if:show_pass>
+                    </div>
+                </div>
+            </bx_repeat:items>
+        </div>
+    </div>
+</div>

+ 4 - 0
modules/boonex/courses/template/nodes_l1.html

@@ -0,0 +1,4 @@
+<div class="bx-courses-nodes-l__level__ flex">
+    <bx_repeat:nodes>__node__</bx_repeat:nodes>
+</div>
+<div class="bx-courses-paginate bx-def-margin-sec-top">__paginate__</div>

+ 45 - 7
modules/boonex/russian/data/langs/bx_courses/ru.xml

@@ -4,9 +4,10 @@
     <string name="_bx_courses_cmts"><![CDATA[Курсы - Комментарии]]></string>
 
     <string name="_bx_courses_option_enable_auto_approve"><![CDATA[Автоматически активировать курс после создания]]></string>
-    <string name="_bx_courses_option_enable_content"><![CDATA[Включить возможность добавления контента в курс]]></string>
+    <string name="_bx_courses_option_enable_content"><![CDATA[Включить возможность добавления контента в курс (экспериментально)]]></string>
     <string name="_bx_courses_option_content_level_max"><![CDATA[Максимальный уровень для узла в древовидной структуре]]></string>
-    <string name="_bx_courses_option_content_modules"><![CDATA[Модули, из которых контент может быть размещен в курсе]]></string>
+    <string name="_bx_courses_option_content_modules_step"><![CDATA[Модули, из которых контент может быть добален в курс как шаг]]></string>
+    <string name="_bx_courses_option_content_modules_attachment"><![CDATA[Модули, из которых контент может быть добален в курс как вложение]]></string>
     <string name="_bx_courses_option_num_connections_quick"><![CDATA[Количество элементов в блоке друзей]]></string>
     <string name="_bx_courses_option_per_page_browse"><![CDATA[Количество элементов на странице]]></string>
     <string name="_bx_courses_option_num_rss"><![CDATA[Количество элементов в ленте RSS]]></string>
@@ -84,6 +85,12 @@
         <string name="_bx_courses_form_content_node_input_sys_title"><![CDATA[Заголовок]]></string>
     <string name="_bx_courses_form_content_node_input_title"><![CDATA[Заголовок]]></string>
     <string name="_bx_courses_form_content_node_input_title_err"><![CDATA[Заголовок обязателен. Пожалуйста, заполните это поле.]]></string>
+    <string name="_bx_courses_form_content_node_input_sys_text"><![CDATA[Описание]]></string>
+    <string name="_bx_courses_form_content_node_input_text"><![CDATA[Описание]]></string>
+    <string name="_bx_courses_form_content_node_input_sys_passing"><![CDATA[Прохождение]]></string>
+    <string name="_bx_courses_form_content_node_input_passing"><![CDATA[Открытие шагов]]></string>
+    <string name="_bx_courses_form_content_node_input_passing_all"><![CDATA[Все сразу]]></string>
+    <string name="_bx_courses_form_content_node_input_passing_alternate"><![CDATA[Поочередное]]></string>
     <string name="_bx_courses_form_content_node_input_sys_do_submit"><![CDATA[Выполнить]]></string>
     <string name="_bx_courses_form_content_node_input_do_submit"><![CDATA[Выполнить]]></string>
     <string name="_bx_courses_form_content_node_input_sys_do_cancel"><![CDATA[Отменить]]></string>
@@ -111,7 +118,9 @@
     <string name="_bx_courses_page_title_join_profile"><![CDATA[Посещать курс]]></string>
     <string name="_bx_courses_page_title_sys_view_profile_closed"><![CDATA[Просмотр закрытого курса]]></string>
     <string name="_bx_courses_page_title_sys_view_profile"><![CDATA[Просмотр курса]]></string>
+    <string name="_bx_courses_page_title_sys_view_profile_node"><![CDATA[Просмотр урока]]></string>
     <string name="_bx_courses_page_title_view_profile"><![CDATA[{display_name}]]></string>
+    <string name="_bx_courses_page_title_view_profile_node"><![CDATA[Урок]]></string>
     <string name="_bx_courses_page_title_browse"><![CDATA[Курсы]]></string>
     <string name="_bx_courses_page_title_browse_by_author"><![CDATA[Курсы {display_name}]]></string>
     <string name="_bx_courses_page_title_browse_featured"><![CDATA[Продвинутые курсы]]></string>
@@ -175,6 +184,8 @@
     <string name="_bx_courses_page_block_title_system_profile_data"><![CDATA[Контент]]></string>
     <string name="_bx_courses_page_block_title_profile_data_link"><![CDATA[Контент курса: <a href="{profile_link}">{display_name}</a>]]></string>
     <string name="_bx_courses_page_block_title_profile_description"><![CDATA[Описание]]></string>
+    <string name="_bx_courses_page_block_title_profile_structure_l1"><![CDATA[Модули]]></string>
+    <string name="_bx_courses_page_block_title_profile_structure_l2"><![CDATA[Темы]]></string>
     <string name="_bx_courses_page_block_title_profile_friends_link"><![CDATA[<a href="{profile_link}">{display_name}</a> - Друзья]]></string>
     <string name="_bx_courses_page_block_title_profile_friends"><![CDATA[Друзья]]></string>
     <string name="_bx_courses_page_block_title_system_profile_friends"><![CDATA[Друзья]]></string>
@@ -197,6 +208,8 @@
     <string name="_bx_courses_page_block_title_joined_profiles"><![CDATA[Мои курсы]]></string>
     <string name="_bx_courses_page_block_title_entry_all_actions"><![CDATA[Все действия]]></string>
     <string name="_bx_courses_page_block_title_entry_social_sharing"><![CDATA[Поделиться]]></string>
+    <string name="_bx_courses_page_block_title_system_entry_node"><![CDATA[Урок]]></string>
+    <string name="_bx_courses_page_block_title_entry_node"><![CDATA[]]></string>
     <string name="_bx_courses_page_block_title_categories"><![CDATA[Категории курсов]]></string>
     <string name="_bx_courses_page_block_title_invite_to_course"><![CDATA[Пригласить студентов на курс]]></string>
     <string name="_bx_courses_page_block_title_search_form"><![CDATA[Форма поиска]]></string>
@@ -331,6 +344,11 @@
     <string name="_bx_courses_menu_item_title_sm_leave_reject"><![CDATA[Отклонить запрос на посещение]]></string>
     <string name="_bx_courses_menu_item_title_sm_leave_cancel"><![CDATA[Отменить запрос на посещение]]></string>
 
+    <string name="_bx_courses_menu_title_content_add"><![CDATA[Добавить контент]]></string>
+    <string name="_bx_courses_menu_set_title_content_add"><![CDATA[Добавить контент]]></string>
+    <string name="_bx_courses_menu_item_title_node_data_steps"><![CDATA[Шаги]]></string>
+    <string name="_bx_courses_menu_item_title_node_data_attachments"><![CDATA[Вложения]]></string>
+
     <string name="_bx_courses_grid_filter_item_title_adm_select_one_filter1"><![CDATA[Все статусы]]></string>
     <string name="_bx_courses_grid_filter_item_title_adm_select_one_filter2"><![CDATA[Любая аудитория]]></string>
     <string name="_bx_courses_grid_filter_item_title_adm_active"><![CDATA[Активные]]></string>
@@ -368,15 +386,17 @@
 
     <string name="_bx_courses_grid_column_title_cnt_active"><![CDATA[]]></string>
     <string name="_bx_courses_grid_column_title_cnt_title"><![CDATA[Заголовок]]></string>
-    <string name="_bx_courses_grid_column_title_cnt_added"><![CDATA[Создано]]></string>
     <string name="_bx_courses_grid_column_title_cnt_cn_l2"><![CDATA[Тем]]></string>
     <string name="_bx_courses_grid_column_title_cnt_cn_l3"><![CDATA[Уроков]]></string>
     <string name="_bx_courses_grid_column_title_cnt_counters"><![CDATA[Инфо]]></string>
     <string name="_bx_courses_grid_column_title_cnt_content_id"><![CDATA[Контент]]></string>
     <string name="_bx_courses_grid_column_title_cnt_content_type"><![CDATA[Тип]]></string>
+    <string name="_bx_courses_grid_column_title_cnt_usage"><![CDATA[Применение]]></string>
+    <string name="_bx_courses_grid_column_title_cnt_added"><![CDATA[Создано]]></string>
     <string name="_bx_courses_grid_action_title_cnt_back"><![CDATA[Назад]]></string>
     <string name="_bx_courses_grid_action_title_cnt_add"><![CDATA[Добавить: {узел}]]></string>
-    <string name="_bx_courses_grid_action_title_cnt_add_content"><![CDATA[Добавить]]></string>
+    <string name="_bx_courses_grid_action_title_cnt_add_step"><![CDATA[Добавить шаг]]></string>
+    <string name="_bx_courses_grid_action_title_cnt_add_attachment"><![CDATA[Добавить вложение]]></string>
     <string name="_bx_courses_grid_action_title_cnt_edit"><![CDATA[Редактировать]]></string>
     <string name="_bx_courses_grid_action_title_cnt_delete"><![CDATA[Удалить]]></string>
     <string name="_bx_courses_popup_title_cnt_add"><![CDATA[Добавить: {node}]]></string>
@@ -412,9 +432,27 @@
     <string name="_bx_courses_txt_n_unit"><![CDATA[{0} ({1})]]></string>
     <string name="_bx_courses_txt_cart_item_title"><![CDATA[{0}: {1} ({2} {3})]]></string>
     <string name="_bx_courses_txt_cart_item_title_lifetime"><![CDATA[{0}: {1} (пожизненно)]]></string>
-    <string name="_bx_courses_txt_module"><![CDATA[модуль]]></string>
-    <string name="_bx_courses_txt_theme"><![CDATA[тема]]></string>
-    <string name="_bx_courses_txt_lesson"><![CDATA[урок]]></string>
+    <string name="_bx_courses_txt_sample_l1_single"><![CDATA[модуль]]></string>
+    <string name="_bx_courses_txt_sample_l1_plural"><![CDATA[модули]]></string>
+    <string name="_bx_courses_txt_sample_l2_single"><![CDATA[тема]]></string>
+    <string name="_bx_courses_txt_sample_l2_plural"><![CDATA[темы]]></string>
+    <string name="_bx_courses_txt_sample_l3_single"><![CDATA[урок]]></string>
+    <string name="_bx_courses_txt_sample_l3_plural"><![CDATA[уроки]]></string>
+    <string name="_bx_courses_txt_status_not_started"><![CDATA[не начато]]></string>
+    <string name="_bx_courses_txt_status_in_process"><![CDATA[в процессе]]></string>
+    <string name="_bx_courses_txt_status_completed"><![CDATA[пройдено]]></string>
+    <string name="_bx_courses_txt_pass_start"><![CDATA[Начать]]></string>
+    <string name="_bx_courses_txt_pass_continue"><![CDATA[Продолжить]]></string>
+    <string name="_bx_courses_txt_pass_again"><![CDATA[Пройти снова]]></string>
+    <string name="_bx_courses_txt_pass"><![CDATA[Пройти]]></string>
+    <string name="_bx_courses_txt_n_m_steps"><![CDATA[{0} / {1} шагов]]></string>
+    <string name="_bx_courses_txt_data_type_bx_files"><![CDATA[файл]]></string>
+    <string name="_bx_courses_txt_data_type_bx_photos"><![CDATA[изображение]]></string>
+    <string name="_bx_courses_txt_data_type_bx_polls"><![CDATA[опрос]]></string>
+    <string name="_bx_courses_txt_data_type_bx_posts"><![CDATA[теория]]></string>
+    <string name="_bx_courses_txt_data_type_bx_videos"><![CDATA[видео]]></string>
+    <string name="_bx_courses_txt_data_usage_0"><![CDATA[шаг]]></string>
+    <string name="_bx_courses_txt_data_usage_1"><![CDATA[вложение]]></string>
 
     <string name="_bx_courses_msg_performed"><![CDATA[Действие было успешно выполнено.]]></string>
     <string name="_bx_courses_err_price_duplicate"><![CDATA[Аналогичный ценовой план уже существует.]]></string>

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
plugins_public/tailwind/css/tailwind.min.css


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů