- Enh #3995: Added additional user profile stream filter to include or exclude non profile stream content

- Enh: Added `humhub\modules\stream\actions\Stream:initQuery` to manage query filter in subclasses
- Fix #4199: Pinned posts of other spaces are excluded from profile stream
This commit is contained in:
buddh4 2020-07-06 14:10:32 +02:00
parent d0dfbcb93e
commit 8841a7bdd4
13 changed files with 302 additions and 133 deletions

View File

@ -13,3 +13,6 @@ 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
- Fix #4199: Pinned posts of other spaces are excluded from profile stream
- Enh #3995: Added additional user profile stream filter to include or exclude non profile stream content
- Enh: Added `humhub\modules\stream\actions\Stream:initQuery` to manage query filter in subclasses

View File

@ -1,29 +1,40 @@
<?php
use humhub\libs\Html;
use humhub\modules\content\helpers\ContentContainerHelper;
use humhub\modules\content\models\Content;
use humhub\modules\activity\models\Activity;
/**
* WallEntry used in a stream and the activity stream.
*
* @property Mixed $object a content object like Post
* @property Content $entry the wall entry to display
* @property String $content the output of the content object (wallOut)
*
* @package humhub.modules_core.wall
* @since 0.5
* @var Mixed $object a content object like Post
* @var string $jsWidget js widget component
* @var Content $entry the wall entry to display
* @var String $content the output of the content object (wallOut)
*/
?>
<?php
$cssClass = ($entry->pinned) ? 'wall-entry pinned-entry' : 'wall-entry';
$isActivity = $entry->object_model == humhub\modules\activity\models\Activity::class;
$container = ContentContainerHelper::getCurrent();
$isPinned = $container && $entry->pinned && $container->contentcontainer_id === $entry->contentcontainer_id;
$isActivity = $entry->object_model === Activity::class;
?>
<?php if (!$isActivity) : ?>
<div class="<?= $cssClass ?>" data-stream-entry data-stream-pinned="<?= $entry->pinned ?>" data-action-component="<?= $jsWidget ?>" data-content-key="<?= $entry->id; ?>" >
<?= Html::beginTag('div', [
'class' => ($isPinned) ? 'wall-entry pinned-entry' : 'wall-entry',
'data' => [
'content-container-id' => $entry->contentcontainer_id,
'stream-entry' => 1,
'stream-pinned' => (int) $isPinned,
'action-component' => $jsWidget,
'content-key' => $entry->id
]
])?>
<?php endif; ?>
<?= $content; ?>
<?= $content ?>
<?php if (!$isActivity) : ?>
</div>
<?= Html::endTag('div')?>
<?php endif; ?>

View File

@ -10,6 +10,8 @@ namespace humhub\modules\stream\actions;
use Yii;
use humhub\modules\content\models\Content;
use yii\db\ArrayExpression;
use yii\db\Expression;
/**
* ContentContainerStream is used to stream contentcontainers (space or users) content.
@ -60,7 +62,7 @@ class ContentContainerStream extends Stream
}
/**
* Make sure pinned contents are returned first.
* Handles ordering of pinned content entries.
*/
protected function handlePinnedContent()
{
@ -69,34 +71,22 @@ class ContentContainerStream extends Stream
// Get number of pinned contents
$pinnedQuery = clone $this->activeQuery;
$pinnedQuery->andWhere(['AND', ['content.pinned' => 1], ['content.contentcontainer_id' => $this->contentContainer->contentcontainer_id]]);
$pinnedCount = $pinnedQuery->count();
$pinnedContent = $pinnedQuery->select('content.id')->column();
// Increase query result limit to ensure there are also not pinned entries
$this->activeQuery->limit += $pinnedCount;
if(!empty($pinnedContent)) {
// Increase query result limit to ensure all pinned entries are included in the first request
$this->activeQuery->limit += count($pinnedContent);
// Modify order - pinned content first
$oldOrder = $this->activeQuery->orderBy;
$this->activeQuery->orderBy("");
$this->activeQuery->addOrderBy('content.pinned DESC');
// Modify order - pinned content first
$oldOrder = $this->activeQuery->orderBy;
$this->activeQuery->orderBy("");
$this->activeQuery->addOrderBy(new Expression('CASE WHEN `content`.`id` IN ('.implode(',', $pinnedContent).') THEN 1 else 0 END DESC'));
$this->activeQuery->addOrderBy($oldOrder);
}
// Only include pinned content of the current contentcontainer (profile stream)
$this->activeQuery->andWhere([
'OR',
[
'AND',
['content.pinned' => 1],
['content.contentcontainer_id' => $this->contentContainer->contentcontainer_id],
],
['content.pinned' => 0],
]);
$this->activeQuery->addOrderBy($oldOrder);
} else {
// No pinned content in further queries
$this->activeQuery->andWhere("content.pinned = 0");
// All pinned entries of this container were loaded within the initial request, so don't include them here!
$this->activeQuery->andWhere(['OR', ['content.pinned' => 0], ['<>', 'content.contentcontainer_id', $this->contentContainer->contentcontainer_id]]);
}
}
}

View File

@ -9,6 +9,7 @@
namespace humhub\modules\stream\actions;
use humhub\modules\content\components\ContentActiveRecord;
use humhub\modules\stream\models\StreamQuery;
use humhub\modules\stream\models\WallStreamQuery;
use humhub\modules\content\models\Content;
use humhub\modules\user\models\User;
@ -62,9 +63,15 @@ abstract class Stream extends Action
const SORT_UPDATED_AT = 'u';
/**
* Modes
* @var string
* @deprecated since 1.6 use ActivityStreamAction
*/
const MODE_NORMAL = 'normal';
/**
* @var string
* @deprecated since 1.6 use ActivityStreamAction
*/
const MODE_ACTIVITY = 'activity';
const FROM_DASHBOARD = 'dashboard';
@ -76,6 +83,7 @@ abstract class Stream extends Action
/**
* @var string
* @deprecated since 1.6 use ActivityStreamAction
*/
public $mode;
@ -165,8 +173,7 @@ abstract class Stream extends Action
{
$this->excludes = array_merge($this->excludes, Yii::$app->getModule('stream')->streamExcludes);
$streamQueryClass = $this->streamQueryClass;
$this->streamQuery = $streamQueryClass::find($this->includes, $this->excludes)->forUser($this->user);
$this->streamQuery = $this->initQuery();
// Read parameters
if (!Yii::$app->request->isConsoleRequest) {
@ -192,6 +199,19 @@ abstract class Stream extends Action
$this->setupFilters();
}
/**
* Initializes the StreamQuery instance. This can be used to add or remove stream filters or set query defaults.
* By default [[streamQueryClass]] property will be used to initialize the instance.
*
* @since 1.6
* @return StreamQuery
*/
protected function initQuery()
{
$streamQueryClass = $this->streamQueryClass;
return $streamQueryClass::find($this->includes, $this->excludes)->forUser($this->user);
}
protected function setActionSettings()
{
// Merge configured filters set for this action with request filters.

View File

@ -2,7 +2,9 @@
namespace humhub\modules\stream\models;
use humhub\modules\stream\models\filters\StreamQueryFilter;
use Yii;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\db\ActiveQuery;
use yii\helpers\ArrayHelper;
@ -206,7 +208,7 @@ class StreamQuery extends Model
/**
* Builder function used to set the user perspective of the stream.
*
* @param $user|null User if null the current user identity will be used
* @param $user |null User if null the current user identity will be used
* @return static
* @see checkUser
*/
@ -414,8 +416,8 @@ class StreamQuery extends Model
*/
protected function checkSort()
{
if(empty($this->sort) || !in_array($this->sort, [Stream::SORT_CREATED_AT, Stream::SORT_UPDATED_AT])) {
$this->sort = Yii::$app->getModule('stream')->settings->get('defaultSort', Stream::SORT_CREATED_AT);
if (empty($this->sort) || !in_array($this->sort, [Stream::SORT_CREATED_AT, Stream::SORT_UPDATED_AT])) {
$this->sort = Yii::$app->getModule('stream')->settings->get('defaultSort', Stream::SORT_CREATED_AT);
}
}
@ -427,7 +429,7 @@ class StreamQuery extends Model
if (empty($this->from)) {
$this->from = null;
} else {
$this->from = (int) $this->from;
$this->from = (int)$this->from;
}
}
@ -439,7 +441,7 @@ class StreamQuery extends Model
if (empty($this->to)) {
$this->to = null;
} else {
$this->to = (int) $this->to;
$this->to = (int)$this->to;
}
}
@ -451,7 +453,7 @@ class StreamQuery extends Model
if (empty($this->limit) || $this->limit > self::MAX_LIMIT) {
$this->limit = self::MAX_LIMIT;
} else {
$this->limit = (int) $this->limit;
$this->limit = (int)$this->limit;
}
}
@ -517,15 +519,47 @@ class StreamQuery extends Model
{
$this->trigger(static::EVENT_BEFORE_FILTER);
foreach ($this->filterHandlers as $handlerClass) {
/** @var $handler QueryFilter **/
Yii::createObject([
'class' => $handlerClass,
foreach ($this->filterHandlers as $handler) {
$this->initHandler($handler)->apply();
}
}
/**
* @param $handler
* @return StreamQueryFilter
* @throws InvalidConfigException
* @since 1.6
*/
public function addFilterHandler($handler)
{
$handler = $this->initHandler($handler);
return $this->filterHandlers[] = $handler;
}
/**
* @param StreamQueryFilter|string $handler
* @return StreamQueryFilter
* @throws InvalidConfigException
* @since 1.6
*/
private function initHandler($handler)
{
if (is_string($handler)) {
$handler = Yii::createObject([
'class' => $handler,
'streamQuery' => $this,
'query' => $this->_query,
'formName' => $this->formName()
])->apply();
]);
} elseif ($handler instanceof StreamQueryFilter) {
$handler->streamQuery = $this;
$handler->query = $this->_query;
$handler->formName = $this->formName();
} else {
throw new InvalidConfigException('Invalid stream filter class');
}
return $handler;
}
/**

View File

@ -20,4 +20,12 @@ abstract class StreamQueryFilter extends QueryFilter
public $streamQuery;
public $autoLoad = self::AUTO_LOAD_GET;
/**
* @inheritDoc
*/
public function formName()
{
return $this->formName ?: 'StreamQuery';
}
}

View File

@ -96,7 +96,7 @@ class WallStreamFilterNavigation extends FilterNavigation
/**
* @var string view
*/
public $view = 'wallStreamFilterNavigation';
public $view = '@stream/widgets/views/wallStreamFilterNavigation';
/**
* @inheritdoc

View File

@ -8,72 +8,13 @@
namespace humhub\modules\user\components;
use humhub\modules\space\models\Space;
use humhub\modules\stream\actions\ContentContainerStream;
use humhub\modules\user\models\User as UserModel;
use humhub\modules\user\Module;
use Yii;
use yii\base\InvalidConfigException;
/**
* ProfileStream
*
* @package humhub\modules\user\components
* @deprecated since 1.6 use \humhub\modules\user\stream\ProfileStream
*/
class ProfileStream extends ContentContainerStream
class ProfileStream extends \humhub\modules\user\stream\ProfileStreamAction
{
/**
* @inheritdoc
*/
protected function handleContentContainer()
{
/** @var Module $module */
$module = Yii::$app->getModule('user');
if ($module->includeAllUserContentsOnProfile && $this->user !== null) {
$profileUser = $this->contentContainer;
if (!$profileUser instanceof UserModel) {
throw new InvalidConfigException('ContentContainer must be related to a User record.');
}
$this->activeQuery->leftJoin('space', 'contentcontainer.pk=space.id AND contentcontainer.class=:spaceClass', [':spaceClass' => Space::class]);
$this->activeQuery->leftJoin('user cuser', 'contentcontainer.pk=cuser.id AND contentcontainer.class=:userClass', [':userClass' => UserModel::class]);
$this->activeQuery->leftJoin('space_membership',
'contentcontainer.pk=space_membership.space_id AND contentcontainer.class=:spaceClass AND space_membership.user_id=:userId',
[':userId' => $this->user->id, ':spaceClass' => Space::class]
);
$this->activeQuery->andWhere([
'OR',
['content.created_by' => $profileUser->id],
['content.contentcontainer_id' => $profileUser->contentcontainer_id]
]);
// Build Access Check based on Space Content Container
$conditionSpace = 'space.id IS NOT NULL AND ('; // space content
$conditionSpace .= ' (space_membership.status=3)'; // user is space member
$conditionSpace .= ' OR (content.visibility=1 AND space.visibility != 0)'; // visibile space and public content
$conditionSpace .= ')';
// Build Access Check based on User Content Container
$conditionUser = 'cuser.id IS NOT NULL AND ('; // user content
$conditionUser .= ' (content.visibility = 1) OR'; // public visible content
$conditionUser .= ' (content.visibility = 0 AND content.contentcontainer_id=' . $this->user->contentContainerRecord->id . ')'; // private content of user
if (Yii::$app->getModule('friendship')->getIsEnabled()) {
$this->activeQuery->leftJoin('user_friendship cff', 'cuser.id=cff.user_id AND cff.friend_user_id=:fuid', [':fuid' => $this->user->id]);
$conditionUser .= ' OR (content.visibility = 0 AND cff.id IS NOT NULL)'; // users are friends
}
$conditionUser .= ')';
// Created content of is always visible
$conditionUser .= 'OR content.created_by=' . $this->user->id;
$this->activeQuery->andWhere("{$conditionSpace} OR {$conditionUser} OR content.contentcontainer_id IS NULL");
$this->handlePinnedContent();
} else {
parent::handleContentContainer();
}
}
}

View File

@ -8,7 +8,7 @@
namespace humhub\modules\user\controllers;
use humhub\modules\user\components\ProfileStream;
use humhub\modules\user\stream\ProfileStreamAction;
use Yii;
use yii\web\HttpException;
use yii\db\Expression;
@ -51,8 +51,7 @@ class ProfileController extends ContentContainerController
{
return [
'stream' => [
'class' => ProfileStream::class,
'mode' => ProfileStream::MODE_NORMAL,
'class' => ProfileStreamAction::class,
'contentContainer' => $this->contentContainer
],
];

View File

@ -0,0 +1,49 @@
<?php
namespace humhub\modules\user\stream;
use humhub\modules\space\models\Space;
use humhub\modules\stream\actions\ContentContainerStream;
use humhub\modules\user\models\User;
use humhub\modules\user\models\User as UserModel;
use humhub\modules\user\Module;
use humhub\modules\user\stream\filters\IncludeAllContributionsFilter;
use Yii;
use yii\base\InvalidConfigException;
/**
* ProfileStream
*
* @package humhub\modules\user\components
*/
class ProfileStreamAction extends ContentContainerStream
{
/**
* @var IncludeAllContributionsFilter
*/
public $includeAllContributionsFilter;
public function initQuery()
{
$query = parent::initQuery();
$this->includeAllContributionsFilter = $query->addFilterHandler(new IncludeAllContributionsFilter(['user' => $this->contentContainer]));
return $query;
}
/**
* @inheritdoc
*/
protected function handleContentContainer()
{
if (!($this->contentContainer instanceof User)) {
throw new InvalidConfigException('ContentContainer must be related to a User record.');
}
if($this->user && $this->includeAllContributionsFilter->isActive()) {
$this->handlePinnedContent();
} else {
parent::handleContentContainer();
}
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace humhub\modules\user\stream\filters;
use humhub\modules\space\models\Space;
use humhub\modules\stream\models\filters\StreamQueryFilter;
use humhub\modules\user\models\User;
use Yii;
class IncludeAllContributionsFilter extends StreamQueryFilter
{
const ID = 'includeAllContributions';
/**
* @var User
*/
public $user;
public $filters = [];
/**
* @inheritdoc
*/
public function rules()
{
return [
[['filters'], 'safe']
];
}
public function apply()
{
if(!$this->isActive()) {
return;
}
$this->query->leftJoin('space', 'contentcontainer.pk=space.id AND contentcontainer.class=:spaceClass', [':spaceClass' => Space::class]);
$this->query->leftJoin('user cuser', 'contentcontainer.pk=cuser.id AND contentcontainer.class=:userClass', [':userClass' => User::class]);
$this->query->leftJoin('space_membership',
'contentcontainer.pk=space_membership.space_id AND contentcontainer.class=:spaceClass AND space_membership.user_id=:userId',
[':userId' => $this->user->id, ':spaceClass' => Space::class]
);
$this->query->andWhere([
'OR',
['content.created_by' => $this->user->id],
['content.contentcontainer_id' => $this->user->contentcontainer_id]
]);
// Build Access Check based on Space Content Container
$conditionSpace = 'space.id IS NOT NULL AND ('; // space content
$conditionSpace .= ' (space_membership.status=3)'; // user is space member
$conditionSpace .= ' OR (content.visibility=1 AND space.visibility != 0)'; // visibile space and public content
$conditionSpace .= ')';
// Build Access Check based on User Content Container
$conditionUser = 'cuser.id IS NOT NULL AND ('; // user content
$conditionUser .= ' (content.visibility = 1) OR'; // public visible content
$conditionUser .= ' (content.visibility = 0 AND content.contentcontainer_id=' . $this->user->contentContainerRecord->id . ')'; // private content of user
if (Yii::$app->getModule('friendship')->getIsEnabled()) {
$this->query->leftJoin('user_friendship cff', 'cuser.id=cff.user_id AND cff.friend_user_id=:fuid', [':fuid' => $this->user->id]);
$conditionUser .= ' OR (content.visibility = 0 AND cff.id IS NOT NULL)'; // users are friends
}
$conditionUser .= ')';
// Created content of is always visible
$conditionUser .= 'OR content.created_by=' . $this->user->id;
$this->query->andWhere("{$conditionSpace} OR {$conditionUser} OR content.contentcontainer_id IS NULL");
}
public function isActive()
{
return in_array(static::ID, $this->filters);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace humhub\modules\user\widgets;
use humhub\modules\stream\widgets\WallStreamFilterNavigation;
use humhub\modules\user\Module;
use humhub\modules\user\stream\filters\IncludeAllContributionsFilter;
use Yii;
class ProfileStreamFilterNavigation extends WallStreamFilterNavigation
{
protected function initFilters()
{
parent::initFilters();
/** @var Module $module */
$module = Yii::$app->getModule('user');
$this->addFilter([
'id' => IncludeAllContributionsFilter::ID,
'title' => Yii::t('UserModule.base', 'Include all content'),
'sortOrder' => 500,
'checked' => $module->includeAllUserContentsOnProfile
], static::FILTER_BLOCK_BASIC);
}
}

View File

@ -15,7 +15,7 @@ use humhub\modules\post\permissions\CreatePost;
/**
* StreamViewer shows a users profile stream
*
*
* @since 1.2.4
* @author Luke
*/
@ -27,32 +27,38 @@ class StreamViewer extends BaseStreamViewer
*/
public $streamAction = '/user/profile/stream';
/**
* @inheritdoc
*/
public $streamFilterNavigation = ProfileStreamFilterNavigation::class;
/**
* @var User
*/
public $contentContainer;
/**
* @inheritdoc
*/
public function init()
{
$createPostPermission = new CreatePost();
parent::init();
if (empty($this->messageStreamEmptyCss)) {
if ($this->contentContainer->permissionManager->can($createPostPermission)) {
$this->messageStreamEmptyCss = 'placeholder-empty-stream';
}
$canCreatePost = $this->contentContainer->permissionManager->can(CreatePost::class);
if (empty($this->messageStreamEmptyCss) && $canCreatePost) {
$this->messageStreamEmptyCss = 'placeholder-empty-stream';
}
if (empty($this->messageStreamEmpty)) {
if ($this->contentContainer->permissionManager->can($createPostPermission)) {
if (Yii::$app->user->id === $this->contentContainer->id) {
$this->messageStreamEmpty = Yii::t('UserModule.profile', '<b>Your profile stream is still empty</b><br>Get started and post something...');
} else {
$this->messageStreamEmpty = Yii::t('UserModule.profile', '<b>This profile stream is still empty</b><br>Be the first and post something...');
}
if ($canCreatePost) {
$this->messageStreamEmpty = $this->contentContainer->is(Yii::$app->user->getIdentity())
? Yii::t('UserModule.profile', '<b>Your profile stream is still empty</b><br>Get started and post something...')
: Yii::t('UserModule.profile', '<b>This profile stream is still empty</b><br>Be the first and post something...');
} else {
$this->messageStreamEmpty = Yii::t('UserModule.profile', '<b>This profile stream is still empty!</b>');
}
}
parent::init();
}
}