From deb2d0e98f82f57295bf4a727af1ae69f7f5a9de Mon Sep 17 00:00:00 2001 From: Peter Rotich <peter@osticket.com> Date: Sat, 18 Mar 2017 19:09:30 +0000 Subject: [PATCH] Add ability to nest Knowledgebase Categories Introduce ability to create sub-categories to knowledge base categories --- include/class.category.php | 92 ++++++++++++++++++++++++++-- include/client/faq-category.inc.php | 22 +++++-- include/client/faq.inc.php | 7 ++- include/client/kb-categories.inc.php | 48 ++++++++++++--- include/staff/categories.inc.php | 2 +- include/staff/category.inc.php | 18 ++++++ include/staff/faq-categories.inc.php | 26 ++++++-- include/staff/faq-category.inc.php | 22 +++++-- include/staff/faq-view.inc.php | 3 +- include/staff/faq.inc.php | 2 +- 10 files changed, 209 insertions(+), 33 deletions(-) diff --git a/include/class.category.php b/include/class.category.php index d8cbb72f2..9a8e1b6d3 100644 --- a/include/class.category.php +++ b/include/class.category.php @@ -21,9 +21,16 @@ class Category extends VerySimpleModel { 'pk' => array('category_id'), 'ordering' => array('name'), 'joins' => array( + 'parent' => array( + 'constraint' => array('category_pid' => 'Category.category_id'), + 'null' => true, + ), + 'children' => array( + 'reverse' => 'Category.parent', + ), 'faqs' => array( 'reverse' => 'FAQ.category' - ), + ), ), ); @@ -36,6 +43,9 @@ class Category extends VerySimpleModel { /* ------------------> Getter methods <--------------------- */ function getId() { return $this->category_id; } function getName() { return $this->name; } + function getFullName() { + return self::getNameById($this->category_id) ?: $this->getLocalName(); + } function getNumFAQs() { return $this->faqs->count(); } function getDescription() { return $this->description; } function getDescriptionWithImages() { return Format::viewableImages($this->description); } @@ -109,6 +119,29 @@ class Category extends VerySimpleModel { ->limit(5); } + function getPublicSubCategories() { + return $this->getSubCategories(array('public' => true)); + } + + function getSubCategories($criteria=array()) { + + $categories = self::objects() + ->filter(array('category_pid' => $this->getId())); + if (isset($criteria['public']) && $categories) { + $categories + ->exclude( + Q::any(array( + 'ispublic'=>Category::VISIBILITY_PRIVATE, + 'faqs__ispublished'=>FAQ::VISIBILITY_PRIVATE, + ))) + ->annotate(array( + 'faq_count'=>SqlAggregate::COUNT('faqs'))) + ->filter(array('faq_count__gt'=>0)); + } + + return $categories; + } + /* ------------------> Setter methods <--------------------- */ function setName($name) { $this->name=$name; } function setNotes($notes) { $this->notes=$notes; } @@ -128,7 +161,7 @@ class Category extends VerySimpleModel { $errors['name'] = __('Category name is required'); elseif (strlen($vars['name']) < 3) $errors['name'] = __('Name is too short. 3 chars minimum'); - elseif (($cid=self::findIdByName($vars['name'])) && $cid != $vars['id']) + elseif (($cid=self::findIdByName($vars['name'], $vars['pid'])) && $cid != $vars['id']) $errors['name'] = __('Category already exists'); if (!$vars['description']) @@ -143,6 +176,7 @@ class Category extends VerySimpleModel { $this->ispublic = $vars['ispublic']; $this->name = $vars['name']; + $this->category_pid = $vars['pid'] ?: 0; $this->description = Format::sanitize($vars['description']); $this->notes = Format::sanitize($vars['notes']); @@ -223,26 +257,72 @@ class Category extends VerySimpleModel { /* ------------------> Static methods <--------------------- */ - static function findIdByName($name) { + static function findIdByName($name, $pid=null) { $row = self::objects()->filter(array( - 'name'=>$name + 'name'=>$name, + 'category_pid' => $pid ?: null ))->values_flat('category_id')->first(); return ($row) ? $row[0] : null; } - static function findByName($name) { + static function findByName($name, $pid=null) { return self::objects()->filter(array( - 'name'=>$name + 'name'=>$name, + 'category_pid' => $pid ?: null ))->one(); } + static function getNameById($id) { + $names = static::getCategories(); + return $names[$id] ?: ''; + } + static function getFeatured() { return self::objects()->filter(array( 'ispublic'=>self::VISIBILITY_FEATURED )); } + static function getCategories($criteria=null, $localize=true) { + static $categories = null; + + if (!isset($categories) || $criteria) { + $categories = array(); + $query = self::objects(); + $query->order_by('name') + ->values('category_id', 'category_pid', 'name', 'parent'); + + foreach ($query as $row) + $categories[$row['category_id']] = $row; + + // Resolve parent names + $names = array(); + foreach ($categories as $id=>$info) { + $name = $info['name']; + $loop = array($id=>true); + $parent = false; + while ($info['category_pid'] && ($info = $categories[$info['category_pid']])) { + $name = sprintf('%s / %s', $info['name'], $name); + if (isset($loop[$info['category_pid']])) + break; + $loop[$info['category_pid']] = true; + $parent = $info; + } + // TODO: localize category names + $names[$id] = $name; + } + asort($names); + + if ($criteria) + return $names; + + $categories = $names; + } + + return $categories; + } + static function create($vars=false) { $category = new static($vars); $category->created = SqlFunction::NOW(); diff --git a/include/client/faq-category.inc.php b/include/client/faq-category.inc.php index 3ce0b7230..2eb0edea6 100644 --- a/include/client/faq-category.inc.php +++ b/include/client/faq-category.inc.php @@ -1,14 +1,26 @@ <?php if(!defined('OSTCLIENTINC') || !$category || !$category->isPublic()) die('Access Denied'); ?> - <div class="row"> <div class="span8"> - <h1><?php echo __('Frequently Asked Questions');?></h1> - <h2><strong><?php echo $category->getLocalName() ?></strong></h2> + <h1><?php echo $category->getFullName(); ?></h1> <p> <?php echo Format::safe_html($category->getLocalDescriptionWithImages()); ?> </p> +<?php + +if (($subs=$category->getSubCategories(array('public' => true)))) { + echo '<div>'; + foreach ($subs as $c) { + echo sprintf('<div><i class="icon-folder-open-alt"></i> + <a href="faq.php?cid=%d">%s (%d)</a></div>', + $c->getId(), + $c->getLocalName(), + $c->getNumFAQs() + ); + } + echo '</div>'; +} ?> <hr> <?php $faqs = FAQ::objects() @@ -22,7 +34,7 @@ $faqs = FAQ::objects() if ($faqs->exists(true)) { echo ' - <h2>'.__('Further Articles').'</h2> + <h2>'.__('Frequently Asked Questions').'</h2> <div id="faq"> <ol>'; foreach ($faqs as $F) { @@ -33,7 +45,7 @@ foreach ($faqs as $F) { } echo ' </ol> </div>'; -}else { +} elseif (!$category->children) { echo '<strong>'.__('This category does not have any FAQs.').' <a href="index.php">'.__('Back To Index').'</a></strong>'; } ?> diff --git a/include/client/faq.inc.php b/include/client/faq.inc.php index c90431562..e953c5475 100644 --- a/include/client/faq.inc.php +++ b/include/client/faq.inc.php @@ -7,10 +7,11 @@ $category=$faq->getCategory(); <div class="row"> <div class="span8"> -<h1><?php echo __('Frequently Asked Questions');?></h1> -<div id="breadcrumbs"> +<h1><?php echo __('Frequently Asked Question');?></h1> +<div id="breadcrumbs" style="padding-top:2px;"> <a href="index.php"><?php echo __('All Categories');?></a> - » <a href="faq.php?cid=<?php echo $category->getId(); ?>"><?php echo $category->getName(); ?></a> + » <a href="faq.php?cid=<?php echo $category->getId(); ?>"><?php + echo $category->getFullName(); ?></a> </div> <div class="faq-content"> diff --git a/include/client/kb-categories.inc.php b/include/client/kb-categories.inc.php index c4df171c5..a9a9f5139 100644 --- a/include/client/kb-categories.inc.php +++ b/include/client/kb-categories.inc.php @@ -4,23 +4,57 @@ $categories = Category::objects() ->exclude(Q::any(array( 'ispublic'=>Category::VISIBILITY_PRIVATE, - 'faqs__ispublished'=>FAQ::VISIBILITY_PRIVATE, + Q::all(array( + 'faqs__ispublished'=>FAQ::VISIBILITY_PRIVATE, + 'children__ispublic' => Category::VISIBILITY_PRIVATE, + 'children__faqs__ispublished'=>FAQ::VISIBILITY_PRIVATE, + )) ))) - ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs'))) - ->filter(array('faq_count__gt'=>0)); + //->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs__ispublished'))); + ->annotate(array('faq_count' => SqlAggregate::COUNT( + SqlCase::N() + ->when(array( + 'faqs__ispublished__gt'=> FAQ::VISIBILITY_PRIVATE), 1) + ->otherwise(null) + ))); + + // ->filter(array('faq_count__gt' => 0)); if ($categories->exists(true)) { ?> <div><?php echo __('Click on the category to browse FAQs.'); ?></div> <ul id="kb"> <?php - foreach ($categories as $C) { ?> + foreach ($categories as $C) { + // Don't show subcategories with parents. + if (($p=$C->parent) + && ($categories->findFirst(array( + 'category_id' => $p->getId())))) + continue; + + ?> <li><i></i> <div style="margin-left:45px"> - <h4><?php echo sprintf('<a href="faq.php?cid=%d">%s (%d)</a>', - $C->getId(), Format::htmlchars($C->getLocalName()), $C->faq_count); ?></h4> + <h4><?php echo sprintf('<a href="faq.php?cid=%d">%s %s</a>', + $C->getId(), Format::htmlchars($C->getFullName()), + $C->faq_count ? "({$C->faq_count})": '' + ); ?></h4> <div class="faded" style="margin:10px 0"> <?php echo Format::safe_html($C->getLocalDescriptionWithImages()); ?> </div> -<?php foreach ($C->faqs +<?php + if (($subs=$C->getPublicSubCategories())) { + echo '<p/><div style="padding-bottom:15px;">'; + foreach ($subs as $c) { + echo sprintf('<div><i class="icon-folder-open"></i> + <a href="faq.php?cid=%d">%s (%d)</a></div>', + $c->getId(), + $c->getLocalName(), + $c->getNumFAQs() + ); + } + echo '</div>'; + } + + foreach ($C->faqs ->exclude(array('ispublished'=>FAQ::VISIBILITY_PRIVATE)) ->limit(5) as $F) { ?> <div class="popular-faq"><i class="icon-file-alt"></i> diff --git a/include/staff/categories.inc.php b/include/staff/categories.inc.php index 91e2419e7..cbfa58402 100644 --- a/include/staff/categories.inc.php +++ b/include/staff/categories.inc.php @@ -95,7 +95,7 @@ $pageNav->paginate($categories); <?php echo $sel?'checked="checked"':''; ?>> </td> <td><a class="truncate" style="width:500px" href="categories.php?id=<?php echo $C->getId(); ?>"><?php - echo $C->getLocalName(); ?></a></td> + echo Category::getNamebyId($C->getId()); ?></a></td> <td><?php echo $C->getVisibilityDescription(); ?></td> <td style="text-align:right;padding-right:25px;"><?php echo $faqs; ?></td> <td> <?php echo Format::datetime($C->updated); ?></td> diff --git a/include/staff/category.inc.php b/include/staff/category.inc.php index 4eac20421..20392a067 100644 --- a/include/staff/category.inc.php +++ b/include/staff/category.inc.php @@ -107,6 +107,24 @@ if (count($langs) > 1) { ?> ?>" id="lang-<?php echo $tag; ?>" <?php if ($i['direction'] == 'rtl') echo 'dir="rtl" class="rtl"'; ?> > + <div style="padding-bottom:8px;"> + <b><?php echo __('Parent');?></b>: + <div class="faded"><?php echo __('Parent Category');?></div> + </div> + <div style="padding-bottom:8px;"> + <select name="pid"> + <option value="">— <?php echo __('Top-Level Category'); ?> —</option> + <?php + foreach (Category::getCategories() as $id=>$name) { + if ($info['id'] && $id == $info['id']) + continue; ?> + <option value="<?php echo $id; ?>" <?php + if ($info['category_pid'] == $id) echo 'selected="selected"'; + ?>><?php echo $name; ?></option> + <?php + } ?> + </select> + </div> <div style="padding-bottom:8px;"> <b><?php echo __('Category Name');?></b>: <span class="error">*</span> diff --git a/include/staff/faq-categories.inc.php b/include/staff/faq-categories.inc.php index 4c4848227..2de840b58 100644 --- a/include/staff/faq-categories.inc.php +++ b/include/staff/faq-categories.inc.php @@ -55,7 +55,7 @@ foreach ($categories as $C) { <i class="icon-fixed-width <?php if ($active) echo 'icon-hand-right'; ?>"></i> <?php echo sprintf('%s (%d)', - Format::htmlchars($C->getLocalName()), + Format::htmlchars($C->getFullName()), $C->faq_count); ?></a> </li> <?php } ?> @@ -140,7 +140,9 @@ if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search. } } else { //Category Listing. $categories = Category::objects() - ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs'))); + ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs'))) + ->filter(array('category_pid__isnull' => true)); + if (count($categories)) { $categories->sort(function($a) { return $a->getLocalName(); }); @@ -150,11 +152,25 @@ if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search. echo sprintf(' <li> <h4><a class="truncate" style="max-width:600px" href="kb.php?cid=%d">%s (%d)</a> - <span>%s</span></h4> - %s - </li>',$C->getId(),$C->getLocalName(),$C->faq_count, + %s ', + $C->getId(),$C->getLocalName(),$C->faq_count, $C->getVisibilityDescription(), Format::safe_html($C->getLocalDescriptionWithImages()) - ); + ); + if ($C->children) { + echo '<p/><div>'; + foreach ($C->children as $c) { + echo sprintf('<div><i class="icon-folder-open-alt"></i> + <a href="kb.php?cid=%d">%s (%d)</a> - <span>%s</span></div>', + $c->getId(), + $c->getLocalName(), + $c->getNumFAQs(), + $c->getVisibilityDescription() + ); + } + echo '</div>'; + } + echo '</li>'; } echo '</ul>'; } else { diff --git a/include/staff/faq-category.inc.php b/include/staff/faq-category.inc.php index 0dcff184f..36bedc6d9 100644 --- a/include/staff/faq-category.inc.php +++ b/include/staff/faq-category.inc.php @@ -36,13 +36,27 @@ echo sprintf('<div class="pull-right flush-right"> </div> <div class="faq-category"> <div style="margin-bottom:10px;"> - <div class="faq-title pull-left"><?php echo $category->getName() ?></div> + <div class="faq-title pull-left"><?php echo $category->getFullName() ?></div> <div class="faq-status inline">(<?php echo $category->isPublic()?__('Public'):__('Internal'); ?>)</div> <div class="clear"><time class="faq"> <?php echo __('Last Updated').' '. Format::daydatetime($category->getUpdateDate()); ?></time></div> </div> <div class="cat-desc has_bottom_border"> - <?php echo Format::display($category->getDescription()); ?> -</div> + <?php echo Format::display($category->getDescription()); + if ($category->children) { + echo '<p/><div>'; + foreach ($category->children as $c) { + echo sprintf('<div><i class="icon-folder-open-alt"></i> + <a href="kb.php?cid=%d">%s (%d)</a> - <span>%s</span></div>', + $c->getId(), + $c->getLocalName(), + $c->getNumFAQs(), + $c->getVisibilityDescription() + ); + } + echo '</div>'; + } + ?> + </div> <?php @@ -61,7 +75,7 @@ if ($faqs->exists(true)) { } echo ' </ol> </div>'; -}else { +} elseif (!$category->children) { echo '<strong>'.__('Category does not have FAQs').'</strong>'; } ?> diff --git a/include/staff/faq-view.inc.php b/include/staff/faq-view.inc.php index 9f46a4994..a5dc4e560 100644 --- a/include/staff/faq-view.inc.php +++ b/include/staff/faq-view.inc.php @@ -29,7 +29,8 @@ if ($thisstaff->hasPerm(FAQ::PERM_MANAGE)) { ?> <div id="breadcrumbs"> <a href="kb.php"><?php echo __('All Categories');?></a> - » <a href="kb.php?cid=<?php echo $category->getId(); ?>"><?php echo $category->getName(); ?></a> + » <a href="kb.php?cid=<?php echo $category->getId(); ?>"><?php + echo $category->getFullName(); ?></a> <span class="faded">(<?php echo $category->isPublic()?__('Public'):__('Internal'); ?>)</span> </div> diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php index 4579deb8d..830dc4645 100644 --- a/include/staff/faq.inc.php +++ b/include/staff/faq.inc.php @@ -63,7 +63,7 @@ $qstr = Http::build_query($qs); <option value="<?php echo $C->getId(); ?>" <?php if ($C->getId() == $info['category_id']) echo 'selected="selected"'; ?>><?php echo sprintf('%s (%s)', - $C->getName(), + Category::getNameById($C->getId()), $C->isPublic() ? __('Public') : __('Private') ); ?></option> <?php } ?> -- GitLab