Skip to content

Commit

Permalink
IBX-6717: Recent activity (#2161)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Adrien Dupuis <adrien.dupuis@ibexa.co>
Co-authored-by: Paweł Niedzielski <pawel.niedzielski@ibexa.co>
Co-authored-by: Bertrand Dunogier <bertrand.dunogier@gmail.com>
Co-authored-by: Tomasz Dąbrowski <64841871+dabrt@users.noreply.github.com>
Co-authored-by: julitafalcondusza <117284672+julitafalcondusza@users.noreply.github.com>
Co-authored-by: Marek Nocoń <mnocon@users.noreply.github.com>
  • Loading branch information
7 people committed Apr 19, 2024
1 parent 9d12036 commit 6cbbb5f
Show file tree
Hide file tree
Showing 35 changed files with 2,560 additions and 2 deletions.
3 changes: 3 additions & 0 deletions code_samples/recent_activity/config/append_to_services.yaml
@@ -0,0 +1,3 @@
services:

App\ActivityLog\ClassNameMapper\MyFeatureNameMapper: ~
@@ -0,0 +1,24 @@
<?php declare(strict_types=1);

namespace App\ActivityLog\ClassNameMapper;

use App\MyFeature\MyFeature;
use Ibexa\Contracts\ActivityLog\ClassNameMapperInterface;
use JMS\TranslationBundle\Model\Message;
use JMS\TranslationBundle\Translation\TranslationContainerInterface;

class MyFeatureNameMapper implements ClassNameMapperInterface, TranslationContainerInterface
{
public function getClassNameToShortNameMap(): iterable
{
yield MyFeature::class => 'my_feature';
}

public static function getTranslationMessages(): array
{
return [
(new Message('ibexa.activity_log.search_form.object_class.my_feature', 'ibexa_activity_log'))
->setDesc('My Feature'),
];
}
}
@@ -0,0 +1,87 @@
<?php declare(strict_types=1);

namespace App\Command;

use App\Event\MyFeatureEvent;
use App\MyFeature\MyFeature;
use Ibexa\Contracts\ActivityLog\ActivityLogServiceInterface;
use Ibexa\Contracts\Core\Repository\ContentService;
use Ibexa\Contracts\Core\Repository\ContentTypeService;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Core\Repository\Values\Content\LocationCreateStruct;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class ActivityLogContextTestCommand extends Command
{
protected static $defaultName = 'doc:test:activity-log-context';

protected static $defaultDescription = 'Test activity log context usage';

private ActivityLogServiceInterface $activityLogService;

private ContentService $contentService;

private ContentTypeService $contentTypeService;

private EventDispatcherInterface $eventDispatcher;

private PermissionResolver $permissionResolver;

private UserService $userService;

public function __construct(
ActivityLogServiceInterface $activityLogService,
ContentService $contentService,
ContentTypeService $contentTypeService,
EventDispatcherInterface $eventDispatcher,
PermissionResolver $permissionResolver,
UserService $userService
) {
parent::__construct(self::$defaultName);
$this->activityLogService = $activityLogService;
$this->contentService = $contentService;
$this->contentTypeService = $contentTypeService;
$this->eventDispatcher = $eventDispatcher;
$this->permissionResolver = $permissionResolver;
$this->userService = $userService;
}

protected function configure(): void
{
$this->addArgument('id', InputArgument::REQUIRED, 'A test number');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$id = $input->getArgument('id');
$this->permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin('admin'));

$this->activityLogService->prepareContext('my_feature', 'Operation description');

$activityLogStruct = $this->activityLogService->build(MyFeature::class, $id, 'init');
$activityLogStruct->setObjectName("My Feature #$id");
$this->activityLogService->save($activityLogStruct);

$contentCreateStruct = $this->contentService->newContentCreateStruct($this->contentTypeService->loadContentTypeByIdentifier('folder'), 'eng-GB');
$contentCreateStruct->setField('name', "My Feature Folder #$id", 'eng-GB');
$locationCreateStruct = new LocationCreateStruct(['parentLocationId' => 2]);
$draft = $this->contentService->createContent($contentCreateStruct, [$locationCreateStruct]);
$this->contentService->publishVersion($draft->versionInfo);

$event = new MyFeatureEvent(new MyFeature(['id' => $id, 'name' => "My Feature #$id"]), 'simulate');
$this->eventDispatcher->dispatch($event);

$activityLogStruct = $this->activityLogService->build(MyFeature::class, $id, 'complete');
$activityLogStruct->setObjectName("My Feature #$id");
$this->activityLogService->save($activityLogStruct);

$this->activityLogService->dismissContext();

return Command::SUCCESS;
}
}
@@ -0,0 +1,36 @@
<?php declare(strict_types=1);

namespace App\Command;

use App\Event\MyFeatureEvent;
use App\MyFeature\MyFeature;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class DispatchMyFeatureEventCommand extends Command
{
protected static $defaultName = 'app:test:throw-my-feature-event';

protected static $defaultDescription = 'Throw/Dispatch a MyFeatureEvent';

private EventDispatcherInterface $eventDispatcher;

public function __construct(EventDispatcherInterface $eventDispatcher)
{
$this->eventDispatcher = $eventDispatcher;
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$event = new MyFeatureEvent(new MyFeature(['id' => 123, 'name' => 'Logged Name']), 'simulate');
$this->eventDispatcher->dispatch($event);

$event = new MyFeatureEvent((object) ['id' => 456, 'name' => 'Some Name'], 'simulate');
$this->eventDispatcher->dispatch($event);

return Command::SUCCESS;
}
}
@@ -0,0 +1,80 @@
<?php declare(strict_types=1);

namespace App\Command;

use Ibexa\Contracts\ActivityLog\ActivityLogServiceInterface;
use Ibexa\Contracts\ActivityLog\Values\ActivityLog\Criterion;
use Ibexa\Contracts\ActivityLog\Values\ActivityLog\Query;
use Ibexa\Contracts\ActivityLog\Values\ActivityLog\SortClause\LoggedAtSortClause;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Core\Repository\Values\Content\Content;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class MonitorRecentContentCreationCommand extends Command
{
protected static $defaultName = 'app:monitor-content-creation';

protected static $defaultDescription = 'List last 10 log entry groups with creations in the last hour';

private ActivityLogServiceInterface $activityLogService;

private PermissionResolver $permissionResolver;

private UserService $userService;

public function __construct(ActivityLogServiceInterface $activityLogService, PermissionResolver $permissionResolver, UserService $userService)
{
$this->permissionResolver = $permissionResolver;
$this->userService = $userService;
$this->activityLogService = $activityLogService;
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$query = new Query([
new Criterion\ObjectCriterion(Content::class),
new Criterion\ActionCriterion([ActivityLogServiceInterface::ACTION_CREATE]),
new Criterion\LoggedAtCriterion(new \DateTime('- 1 hour'), Criterion\LoggedAtCriterion::GTE),
], [new LoggedAtSortClause(LoggedAtSortClause::DESC)], 0, 10);

$io = new SymfonyStyle($input, $output);

$this->permissionResolver->setCurrentUserReference($this->userService->loadUserByLogin('admin'));

foreach ($this->activityLogService->findGroups($query) as $activityLogGroup) {
if ($activityLogGroup->getSource()) {
$io->section($activityLogGroup->getSource()->getName());
}
if ($activityLogGroup->getDescription()) {
$io->text($activityLogGroup->getDescription());
}
$table = [];
foreach ($activityLogGroup->getActivityLogs() as $activityLog) {
/** @var \Ibexa\Contracts\Core\Repository\Values\Content\Content $content */
$content = $activityLog->getRelatedObject();
$name = $content && $content->getName() && $content->getName() !== $activityLog->getObjectName() ? "{$content->getName()}” (formerly “{$activityLog->getObjectName()}”)" : "{$activityLog->getObjectName()}";
$table[] = [
$activityLogGroup->getLoggedAt()->format(\DateTime::ATOM),
$activityLog->getObjectId(),
$name,
$activityLog->getAction(),
$activityLogGroup->getUser()->login,
];
}
$io->table([
'Logged at',
'Obj. ID',
'Object Name',
'Action',
'User',
], $table);
}

return Command::SUCCESS;
}
}
28 changes: 28 additions & 0 deletions code_samples/recent_activity/src/Event/MyFeatureEvent.php
@@ -0,0 +1,28 @@
<?php declare(strict_types=1);

namespace App\Event;

use Symfony\Contracts\EventDispatcher\Event;

class MyFeatureEvent extends Event
{
private object $object;

private string $action;

public function __construct(object $object, string $action)
{
$this->object = $object;
$this->action = $action;
}

public function getObject(): object
{
return $this->object;
}

public function getAction(): string
{
return $this->action;
}
}
@@ -0,0 +1,36 @@
<?php declare(strict_types=1);

namespace App\EventSubscriber;

use App\Event\MyFeatureEvent;
use Ibexa\Contracts\ActivityLog\ActivityLogServiceInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class MyFeatureEventSubscriber implements EventSubscriberInterface
{
private ActivityLogServiceInterface $activityLogService;

public function __construct(ActivityLogServiceInterface $activityLogService)
{
$this->activityLogService = $activityLogService;
}

public static function getSubscribedEvents(): array
{
return [
MyFeatureEvent::class => 'onMyFeatureEvent',
];
}

public function onMyFeatureEvent(MyFeatureEvent $event): void
{
/** @var App\MyFeature\MyFeature $object */
$object = $event->getObject();
$className = get_class($object);
$id = (string)$object->id;
$action = $event->getAction();
$activityLog = $this->activityLogService->build($className, $id, $action);
$activityLog->setObjectName($object->name);
$this->activityLogService->save($activityLog);
}
}
@@ -0,0 +1,56 @@
<?php declare(strict_types=1);

namespace App\EventSubscriber;

use App\MyFeature\MyFeature;
use App\MyFeature\MyFeatureService;
use Ibexa\Contracts\ActivityLog\Event\PostActivityGroupListLoadEvent;
use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
use Ibexa\Contracts\Core\Repository\Exceptions\UnauthorizedException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class MyFeaturePostActivityListLoadEventSubscriber implements EventSubscriberInterface
{
private MyFeatureService $myFeatureService;

public function __construct(
MyFeatureService $myFeatureService
) {
$this->myFeatureService = $myFeatureService;
}

public static function getSubscribedEvents(): array
{
return [
PostActivityGroupListLoadEvent::class => ['loadMyFeature'],
];
}

public function loadMyFeature(PostActivityGroupListLoadEvent $event): void
{
$visitedIds = [];
$list = $event->getList();
foreach ($list as $logGroup) {
foreach ($logGroup->getActivityLogs() as $log) {
if ($log->getObjectClass() !== MyFeature::class) {
continue;
}

$id = (int)$log->getObjectId();
try {
if (!array_key_exists($id, $visitedIds)) {
$visitedIds[$id] = $this->myFeatureService->load($id);
}

if ($visitedIds[$id] === null) {
continue;
}

$log->setRelatedObject($visitedIds[$id]);
} catch (NotFoundException|UnauthorizedException $e) {
$visitedIds[$id] = null;
}
}
}
}
}
13 changes: 13 additions & 0 deletions code_samples/recent_activity/src/MyFeature/MyFeature.php
@@ -0,0 +1,13 @@
<?php declare(strict_types=1);

namespace App\MyFeature;

class MyFeature extends \stdClass
{
public function __construct(array $properties)
{
foreach ($properties as $propertyName => $propertyValue) {
$this->$propertyName = $propertyValue;
}
}
}
11 changes: 11 additions & 0 deletions code_samples/recent_activity/src/MyFeature/MyFeatureService.php
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);

namespace App\MyFeature;

class MyFeatureService
{
public function load(int $myFeatureId)
{
return new MyFeature(['id' => $myFeatureId, 'name' => 'Actual Name']);
}
}
@@ -0,0 +1,5 @@
{% extends '@IbexaActivityLog/themes/admin/activity_log/ui/default.html.twig' %}

{%- block activity_log_description_widget -%}
{{ dump(log) }}
{%- endblock activity_log_description_widget -%}

0 comments on commit 6cbbb5f

Please sign in to comment.