- Enh #4213: Only render topic chooser if there are topics available or user can create topics

- Enh: Added `humhub\modules\ui\form\widgets\ActiveField:preventRendering` to manage render state within field classes
- Enh: Added `humhub\modules\ui\form\widgets\JsInputWidget:emptyResult()` helper to manage render state of JsInputWidget
- Enh: Added `humhub\modules\ui\form\widgets\JsInputWidget:field` in order to access ActiveField instances within JsInputWidget
This commit is contained in:
buddh4 2020-07-10 16:24:42 +02:00
parent 5ebaa01686
commit 9ef6987034
29 changed files with 615 additions and 12 deletions

View File

@ -13,3 +13,7 @@ HumHub Change Log
- Chg #4158: Cleanup post table removed unused column
- Fix #4182: Native edge password reveal icons interferes with custom one
- Fix #4173: Notification overview HTML compliant issue
- Enh #4213: Only render topic chooser if there are topics available or user can create topics
- Enh: Added `humhub\modules\ui\form\widgets\ActiveField:preventRendering` to manage render state within field classes
- Enh: Added `humhub\modules\ui\form\widgets\JsInputWidget:emptyResult()` helper to manage render state of JsInputWidget
- Enh: Added `humhub\modules\ui\form\widgets\JsInputWidget:field` in order to access ActiveField instances within JsInputWidget

View File

@ -31,6 +31,8 @@ class ContentTagDropDown extends JsInputWidget
public function int()
{
parent::init();
if (!$this->tagClass) {
$this->tagClass = ContentTag::class;
// Reset default behavior inf no specific tagClass is given
@ -49,7 +51,7 @@ class ContentTagDropDown extends JsInputWidget
$items = $this->getItems();
if (empty($items)) {
return;
return $this->emptyResult();
}
$options = $this->getOptions();

View File

@ -97,9 +97,11 @@ $pickerUrl = ($contentContainer instanceof Space) ? $contentContainer->createUrl
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Notify members'), 'notifyUser')->icon('fa-bell')?>
</li>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
</li>
<?php if(TopicPicker::showTopicPicker($contentContainer)) : ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
</li>
<?php endif; ?>
<?php if ($canSwitchVisibility): ?>
<li>
<?= Link::withAction(Yii::t('ContentModule.base', 'Make public'), 'changeVisibility')
@ -120,4 +122,4 @@ $pickerUrl = ($contentContainer instanceof Space) ? $contentContainer->createUrl
<?= Html::endForm(); ?>
</div>
<!-- /panel body -->
</div> <!-- /panel -->
</div> <!-- /panel -->

View File

@ -11,6 +11,7 @@ namespace humhub\modules\topic;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\topic\models\Topic;
use humhub\modules\topic\widgets\ContentTopicButton;
use humhub\modules\topic\widgets\TopicPicker;
use humhub\modules\ui\menu\MenuLink;
use humhub\modules\user\events\UserEvent;
use humhub\modules\user\widgets\AccountMenu;
@ -24,7 +25,7 @@ class Events extends BaseObject
/** @var ContentActiveRecord $record */
$record = $event->sender->object;
if ($record->content->canEdit()) {
if ($record->content->canEdit() && TopicPicker::showTopicPicker($record->content->container)) {
$event->sender->addWidget(ContentTopicButton::class, ['record' => $record], ['sortOrder' => 370]);
}
}

View File

@ -0,0 +1,31 @@
actor: Tester
namespace: topic
bootstrap: _bootstrap.php
coverage:
c3_url: 'http://localhost:8080/index-test.php'
enabled: true
remote: false
include:
- ../models/*
- ../widgets/*
- ../Module.php
settings:
suite_class: \PHPUnit_Framework_TestSuite
colors: true
shuffle: false
memory_limit: 1024M
log: true
# This value controls whether PHPUnit attempts to backup global variables
# See https://phpunit.de/manual/current/en/appendixes.annotations.html#appendixes.annotations.backupGlobals
backup_globals: true
paths:
tests: codeception
log: codeception/_output
data: codeception/_data
helpers: codeception/_support
envs: ../../../tests/config/env
config:
# the entry script URL (with host info) for functional and acceptance tests
# PLEASE ADJUST IT TO THE ACTUAL ENTRY SCRIPT URL
test_entry_url: http://localhost:8080/index-test.php

View File

@ -0,0 +1,49 @@
<?php
/**
* This is the initial test bootstrap, which will load the default test bootstrap from the humhub core
*/
// Parse the environment arguments (Note: only simple --env ENV is supported no comma sepration merge...)
$env = isset($GLOBALS['env']) ? $GLOBALS['env'] : [];
// If environment was set try loading special environment config else load default
if (count($env) > 0) {
\Codeception\Configuration::append(['environment' => $env]);
$envCfgFile = dirname(__DIR__) . '/config/env/test.' . $env[0][0] . '.php';
if (file_exists($envCfgFile)) {
$cfg = array_merge(require_once(__DIR__ . '/../config/test.php'), require_once($envCfgFile));
}
}
// If no environment is set we have to load the default config
if (!isset($cfg)) {
$cfg = require_once(__DIR__ . '/../config/test.php');
}
// If no humhub_root is given we assume our module is in the a root to be in /protected/humhub/modules/<module>/tests/codeception directory
$cfg['humhub_root'] = isset($cfg['humhub_root']) ? $cfg['humhub_root'] : dirname(__DIR__) . '/../../../../..';
// Load default test bootstrap
require_once($cfg['humhub_root'] . '/protected/humhub/tests/codeception/_bootstrap.php');
// Overwrite the default test alias
Yii::setAlias('@tests', dirname(__DIR__));
Yii::setAlias('@env', '@tests/config/env');
Yii::setAlias('@root', $cfg['humhub_root']);
Yii::setAlias('@humhubTests', $cfg['humhub_root'] . '/protected/humhub/tests');
// Load all supporting test classes needed for test execution
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_support'));
\Codeception\Util\Autoload::addNamespace('tests\codeception\fixtures', Yii::getAlias('@humhubTests/codeception/fixtures'));
\Codeception\Util\Autoload::addNamespace('', Yii::getAlias('@humhubTests/codeception/_pages'));
if(isset($cfg['modules'])) {
\Codeception\Configuration::append(['humhub_modules' => $cfg['modules']]);
}
if(isset($cfg['fixtures'])) {
\Codeception\Configuration::append(['fixtures' => $cfg['fixtures']]);
}
?>

View File

@ -0,0 +1,4 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore

View File

@ -0,0 +1,26 @@
<?php
namespace topic;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = null)
*
* @SuppressWarnings(PHPMD)
*/
class AcceptanceTester extends \AcceptanceTester
{
use _generated\AcceptanceTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,26 @@
<?php
namespace topic;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = null)
*
* @SuppressWarnings(PHPMD)
*/
class FunctionalTester extends \FunctionalTester
{
use _generated\FunctionalTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,26 @@
<?php
namespace topic;
/**
* Inherited Methods
* @method void wantToTest($text)
* @method void wantTo($text)
* @method void execute($callable)
* @method void expectTo($prediction)
* @method void expect($prediction)
* @method void amGoingTo($argumentation)
* @method void am($role)
* @method void lookForwardTo($achieveValue)
* @method void comment($description)
* @method \Codeception\Lib\Friend haveFriend($name, $actorClass = null)
*
* @SuppressWarnings(PHPMD)
*/
class UnitTester extends \UnitTester
{
use _generated\UnitTesterActions;
/**
* Define custom actions here
*/
}

View File

@ -0,0 +1,22 @@
# Codeception Test Suite Configuration
# suite for acceptance tests.
# perform tests in browser using the Selenium-like tools.
# powered by Mink (http://mink.behat.org).
# (tip: that's what your customer will see).
# (tip: test your ajax and javascript by one of Mink drivers).
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
class_name: AcceptanceTester
modules:
enabled:
- WebDriver
- tests\codeception\_support\WebHelper
- tests\codeception\_support\DynamicFixtureHelper
config:
WebDriver:
url: 'http://localhost:8080/'
browser: chrome
window_size: maximize
port: 4444

View File

@ -0,0 +1,6 @@
<?php
/**
* Initialize the HumHub Application for functional testing. The default application configuration for this suite can be overwritten
* in @tests/config/functional.php
*/
require(Yii::getAlias('@humhubTests/codeception/acceptance/_bootstrap.php'));

View File

@ -0,0 +1,3 @@
<?php
return \tests\codeception\_support\HumHubTestConfiguration::getSuiteConfig('functional');

View File

@ -0,0 +1,3 @@
<?php
return \tests\codeception\_support\HumHubTestConfiguration::getSuiteConfig('unit');

View File

@ -0,0 +1,18 @@
# Codeception Test Suite Configuration
# suite for functional (integration) tests.
# emulate web requests and make application process them.
# (tip: better to use with frameworks).
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
class_name: FunctionalTester
modules:
enabled:
- Filesystem
- Yii2
- tests\codeception\_support\TestHelper
- tests\codeception\_support\DynamicFixtureHelper
- tests\codeception\_support\HumHubHelper
config:
Yii2:
configFile: 'codeception/config/functional.php'

View File

@ -0,0 +1,6 @@
<?php
/**
* Initialize the HumHub Application for functional testing. The default application configuration for this suite can be overwritten
* in @tests/config/functional.php
*/
require(Yii::getAlias('@humhubTests/codeception/functional/_bootstrap.php'));

View File

@ -0,0 +1,14 @@
# Codeception Test Suite Configuration
# suite for unit (internal) tests.
# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
class_name: UnitTester
modules:
enabled:
- tests\codeception\_support\CodeHelper
- Yii2
config:
Yii2:
configFile: 'codeception/config/unit.php'
transaction: false

View File

@ -0,0 +1,68 @@
<?php
namespace tests\codeception\unit;
use humhub\modules\post\models\Post;
use humhub\modules\space\models\Space;
use humhub\modules\topic\models\Topic;
use humhub\modules\topic\widgets\TopicPicker;
use tests\codeception\_support\HumHubDbTestCase;
class TopicPickerTest extends HumHubDbTestCase
{
/**
* Make sure users with create topic permission sees topic picker
*/
public function testUserWithCreateTopicPermissionSeesTopicPickerWithSpaceTopics()
{
// User2 is moderator in Space3
$space = Space::findOne(3);
$this->becomeUser('User2');
$topic = new Topic($space);
$topic->name = 'TestTopic';
$this->assertTrue($topic->save());
$this->assertTrue(TopicPicker::showTopicPicker($space));
}
/**
* Make sure users with create topic permission sees topic picker even if there are no topics available
*/
public function testUserWithCreateTopicPermissionSeesTopicPickerWithoutSpaceTopics()
{
// User2 is moderator in Space3
$space = Space::findOne(3);
$this->becomeUser('User2');
$this->assertTrue(TopicPicker::showTopicPicker($space));
}
/**
* Make sure users without create topic permission sees topic picker if topics are available
*/
public function testUserWithoutCreateTopicPermissionSeesTopicPickerWithSpaceTopics()
{
// User1 is member in Space3
$space = Space::findOne(3);
$this->becomeUser('User1');
$topic = new Topic($space);
$topic->name = 'TestTopic';
$this->assertTrue($topic->save());
$this->assertTrue(TopicPicker::showTopicPicker($space));
}
/**
* Make sure users without create topic permission does not sees topic picker if there are no topics available
*/
public function testUserWithoutCreateTopicPermissionDoesNotSeesTopicPickerWithoutSpaceTopics()
{
// User1 is member in Space3
$space = Space::findOne(3);
$this->becomeUser('User1');
$this->assertFalse(TopicPicker::showTopicPicker($space));
}
}

View File

@ -0,0 +1,119 @@
<?php
namespace tests\codeception\unit;
use humhub\modules\post\models\Post;
use humhub\modules\space\models\Space;
use humhub\modules\topic\models\Topic;
use tests\codeception\_support\HumHubDbTestCase;
class TopicTest extends HumHubDbTestCase
{
/**
* Make sure space admin is allowed to create content by default
* @throws \yii\base\Exception
*/
public function testSpaceAdminCanCreateTopic()
{
// User2 is moderator in Space3
$space = Space::findOne(3);
$this->becomeUser('Admin');
$post = new Post($space, ['message' => 'Test Post']);
$this->assertTrue($post->save());
Topic::attach($post->content, ['_add:NewTopic']);
$topics = Topic::findByContent($post->content)->all();
$this->assertCount(1, $topics);
$this->assertEquals('NewTopic', $topics[0]->name);
}
/**
* Make sure moderator is allowed to create content by default
* @throws \yii\base\Exception
*/
public function testSpaceModeratorCanCreateTopic()
{
// User2 is moderator in Space3
$space = Space::findOne(3);
$this->becomeUser('User2');
$post = new Post($space, ['message' => 'Test Post']);
$this->assertTrue($post->save());
Topic::attach($post->content, ['_add:NewTopic']);
$topics = Topic::findByContent($post->content)->all();
$this->assertCount(1, $topics);
$this->assertEquals('NewTopic', $topics[0]->name);
}
/**
* Make sure user is not allowed to create content by default
* @throws \yii\base\Exception
*/
public function testSpaceMemberCanNotCreateTopic()
{
// User1 is member in Space3
$space = Space::findOne(3);
$this->becomeUser('User1');
$post = new Post($space, ['message' => 'Test Post']);
$this->assertTrue($post->save());
Topic::attach($post->content, ['_add:NewTopic']);
$topics = Topic::findByContent($post->content)->all();
$this->assertEmpty($topics);
}
/**
* Make sure user is not allowed to create content by default
* @throws \yii\base\Exception
*/
public function testAttachTopicByInstance()
{
// User2 is moderator in Space3
$space = Space::findOne(3);
$this->becomeUser('User2');
$post = new Post($space, ['message' => 'Test Post']);
$this->assertTrue($post->save());
$topic = new Topic($space);
$topic->name = 'NewTopic';
$this->assertTrue($topic->save());
Topic::attach($post->content, [$topic]);
$topics = Topic::findByContent($post->content)->all();
$this->assertCount(1, $topics);
$this->assertEquals('NewTopic', $topics[0]->name);
}
/**
* Make sure user is not allowed to create content by default
* @throws \yii\base\Exception
*/
public function testAttachTopicById()
{
// User2 is moderator in Space3
$space = Space::findOne(3);
$this->becomeUser('User2');
$post = new Post($space, ['message' => 'Test Post']);
$this->assertTrue($post->save());
$topic = new Topic($space);
$topic->name = 'NewTopic';
$this->assertTrue($topic->save());
Topic::attach($post->content, [$topic->id]);
$topics = Topic::findByContent($post->content)->all();
$this->assertCount(1, $topics);
$this->assertEquals('NewTopic', $topics[0]->name);
}
}

View File

@ -0,0 +1,6 @@
<?php
/**
* Initialize the HumHub Application for functional testing. The default application configuration for this suite can be overwritten
* in @tests/config/functional.php
*/
require(Yii::getAlias('@humhubTests/codeception/unit/_bootstrap.php'));

View File

@ -0,0 +1,5 @@
<?php
/**
* This config is shared by all suites (unit/functional/acceptance) and can be overwritten by a suite config (e.g. functional.php)
*/
return [];

View File

@ -0,0 +1,5 @@
<?php
/**
* Here you can overwrite the default config for the functional suite. The default config resides in @humhubTests/codeception/config/config.php
*/
return [];

View File

@ -0,0 +1,5 @@
<?php
return [
'fixtures' => ['default']
];

View File

@ -0,0 +1,5 @@
<?php
/**
* Here you can overwrite your functional humhub config. The default config resiedes in @humhubTests/codeception/config/config.php
*/
return [];

View File

@ -8,6 +8,7 @@
namespace humhub\modules\topic\widgets;
use humhub\modules\content\components\ContentContainerActiveRecord;
use humhub\modules\content\helpers\ContentContainerHelper;
use humhub\modules\topic\permissions\AddTopic;
use humhub\modules\content\widgets\ContentTagPicker;
@ -15,6 +16,10 @@ use humhub\modules\topic\models\Topic;
use Yii;
use yii\helpers\Url;
/**
* This InputWidget class can be used to add a topic picker input field. The topic picker field is only
* rendered if there are topics available or if the user is allowed to create topics.
*/
class TopicPicker extends ContentTagPicker
{
/**
@ -37,7 +42,7 @@ class TopicPicker extends ContentTagPicker
*/
public function init()
{
$this->contentContainer = $this->contentContainer ? $this->contentContainer : ContentContainerHelper::getCurrent();
$this->contentContainer = $this->contentContainer ?: ContentContainerHelper::getCurrent();
if (!$this->url && $this->contentContainer) {
$this->url = $this->contentContainer->createUrl('/topic/topic/search');
@ -45,16 +50,71 @@ class TopicPicker extends ContentTagPicker
$this->url = Url::to(['/topic/topic/search']);
}
$this->addOptions = $this->contentContainer && $this->contentContainer->can(AddTopic::class);
$this->addOptions = static::canAddTopic($this->contentContainer);
parent::init();
}
/**
* @inheritdoc
*/
public function run()
{
if(!static::canAddTopic($this->contentContainer) && !static::hasTopics($this->contentContainer)) {
return $this->emptyResult();
}
return parent::run();
}
/**
* Determines if a topicpicker should be rendered for the current user. This is only the case if there are topics
* available for the given container or the user is allowed to create topics.
*
* @param ContentContainerActiveRecord|null $container
* @return bool
*/
public static function showTopicPicker(ContentContainerActiveRecord $container = null)
{
return static::canAddTopic($container) || static::hasTopics($container);
}
/**
* Determines if the current user is allowed to add topics on this container.
*
* @return bool
* @since 1.6
*/
private static function canAddTopic(ContentContainerActiveRecord $container = null)
{
return $container && $container->can(AddTopic::class);
}
/**
* Checks if there are topics available on this container.
*
* @return bool
*/
private static function hasTopics(ContentContainerActiveRecord $container = null)
{
if(!$container) {
return (bool) Topic::find()->count();
}
return (bool) Topic::findByContainer($container)->count();
}
/**
* @inheritdoc
*/
public function getItemImage($item)
{
return Yii::$app->getModule('topic')->icon;
}
/**
* @inheritdoc
*/
protected function getData()
{
$result = parent::getData();

View File

@ -17,6 +17,13 @@ namespace humhub\modules\ui\form\widgets;
*/
class ActiveField extends \yii\bootstrap\ActiveField
{
/**
* @var bool Can be set to true in order to prevent this field from being rendered. This may be used by InputWidgets
* or other fields responsible for custom visibility management.
*
* @since 1.6
*/
public $preventRendering = false;
/**
* @inheritdoc
@ -28,11 +35,50 @@ class ActiveField extends \yii\bootstrap\ActiveField
$config['attribute'] = $this->attribute;
$config['view'] = $this->form->getView();
if (isset($config['options']) && isset(class_parents($class)['humhub\widgets\InputWidget'])) {
$this->adjustLabelFor($config['options']);
if(is_subclass_of($class, JsInputWidget::class)) {
if(isset($config['options'])) {
$this->adjustLabelFor($config['options']);
}
$config['field'] = $this;
}
return parent::widget($class, $config);
}
/**
* @inheritdoc
*/
public function begin()
{
if($this->preventRendering) {
return '';
}
return parent::begin();
}
/**
* @inheritdoc
*/
public function render($content = null)
{
if($this->preventRendering) {
return '';
}
return parent::render($content);
}
/**
* @inheritdoc
*/
public function end()
{
if($this->preventRendering) {
return '';
}
return parent::end();
}
}

View File

@ -26,7 +26,7 @@ class ActiveForm extends \yii\bootstrap\ActiveForm
/**
* @inheritdoc
*/
public $fieldClass = 'humhub\modules\ui\form\widgets\ActiveField';
public $fieldClass = ActiveField::class;
public $acknowledge = false;

View File

@ -45,7 +45,7 @@ class ColorPicker extends JsInputWidget
*/
public function init()
{
if (!empty($this->field)) {
if (!empty($this->field) && is_array($this->field)) {
$this->attribute = $this->field;
}

View File

@ -72,6 +72,13 @@ abstract class JsInputWidget extends JsWidget
*/
public $options = [];
/**
* @var \yii\widgets\ActiveField active input field, which triggers this widget rendering.
* This field will be automatically filled up in case widget instance is created via [[\yii\widgets\ActiveField::widget()]].
* @since 1.6
*/
public $field;
/**
* Initializes the widget.
* If you override this method, make sure you call the parent implementation first.
@ -87,9 +94,43 @@ abstract class JsInputWidget extends JsWidget
if (!$this->id && !isset($this->options['id'])) {
$this->id = $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(true);
}
return true;
}
/**
* Should be returned by [[run]] in order to prevent rendering the field.
* This function will prepare the ActiveField instance by resetting the template and label and return
* an empty string.
*
* ```php
* public function run()
* {
* if(!$this->shouldRender()) {
* return $this->emptyResult();
* }
*
* return parent::run();
* }
* ```
* @return string
* @since 1.6
*/
protected function emptyResult()
{
if($this->field) {
$this->field->label(false);
// Prevents empty-help/error block rendering
$this->field->template = '';
if($this->field instanceof ActiveField) {
$this->field->preventRendering = true;
}
}
return '';
}
/**
* @return string the field value either by extracting from model or if no model is given `$this->value`
* @since 1.3