Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-6717: Recent activity #2161

Merged
merged 57 commits into from Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
3fd7e62
Recent activity: TOC and skeleton
Oct 6, 2023
19bc106
Recent activity: TOC and skeleton
Oct 6, 2023
0fbc3ae
Recent activity: TOC and skeleton
Oct 6, 2023
436d6f3
Recent activity: actual REST API doesn't offer to create entries.
Oct 9, 2023
cbe062d
(minor) Recent activity: Fix Ibexa.DomainTermCapitalization
Oct 10, 2023
82a0948
Recent activity: Draft intro; Add Configuration skeleton
Oct 10, 2023
3438dc0
recent_activity.md: Add product variant as logged
Oct 23, 2023
ea283de
recent_activity.md: truncate_after_days & ibexa:activity-log:truncate
Dec 1, 2023
efaf7f2
recent_activity.md: Simplify intro w/ link to user doc
Dec 4, 2023
db48042
recent_activity.md: More about grouping
Dec 11, 2023
a3626c6
Merge branch 'master' into activity-log_toc
Jan 29, 2024
a66bc84
recent_activity.md: activity_logs_limit is also about groups
Jan 30, 2024
5d01713
Recent activity's policy
Feb 6, 2024
7f8984a
recent_activity.md: Details "Activity Log / Read" policy mandatoriness.
Feb 7, 2024
2137d2c
Merge branch 'master' into activity-log_toc
Feb 8, 2024
2063ff4
policies.md & limitation_reference.md: + ActivityLogOwner
Feb 8, 2024
7f94717
recent_activity.md: Fix vale
Feb 8, 2024
46d58c2
recent_activity.md: Update REST API Ref anchor link
Feb 13, 2024
4120bb6
Update docs/administration/recent_activity/recent_activity.md
adriendupuis Feb 13, 2024
551a3fd
Merge branch 'master' into activity-log_toc
Feb 13, 2024
5a665ba
Merge branch 'master' into activity-log_toc
Feb 13, 2024
ac2aa44
IBX-6742: Recent activity PHP API (#2159)
adriendupuis Feb 15, 2024
3dcea6d
IBX-6737: Recent activity REST API Ref (#2157)
adriendupuis Feb 15, 2024
87c4bb9
Merge branch 'master' into activity-log_toc
Feb 15, 2024
58317f3
Apply suggestions from code review
adriendupuis Feb 15, 2024
0934ee3
Apply suggestions from code review
adriendupuis Feb 16, 2024
1eed4c5
recent_activity.md: Enhance menu items' format
adriendupuis Feb 23, 2024
869069f
Merge branch 'master' into activity-log_toc
adriendupuis Feb 26, 2024
317d66d
Edition badges
adriendupuis Feb 26, 2024
e45d7e7
Apply suggestions from code review
adriendupuis Feb 28, 2024
2f293ed
Apply suggestions from code review
adriendupuis Feb 28, 2024
039be05
Apply suggestions from code review
adriendupuis Feb 28, 2024
5759b6b
Update docs/administration/recent_activity/recent_activity.md
adriendupuis Feb 28, 2024
f5f09d5
Update docs/administration/recent_activity/recent_activity.md
adriendupuis Feb 28, 2024
e608d47
Update docs/administration/recent_activity/recent_activity.md
adriendupuis Feb 28, 2024
52da20e
Update docs/administration/recent_activity/recent_activity.md
adriendupuis Feb 28, 2024
7a34168
Apply suggestions from code review
adriendupuis Feb 28, 2024
ec2664d
Apply suggestions from code review
adriendupuis Apr 9, 2024
2dbab6e
recent_activity.md: Move from parameter to semantic conf
adriendupuis Apr 9, 2024
3186ec7
recent_activity.md: Illustrate context group folding
adriendupuis Apr 9, 2024
494fce9
MyFeaturePostActivityListLoadEventSubscriber: Fix class
adriendupuis Apr 9, 2024
4b4c5fa
recent_activity.md: Context group screenshot using example
adriendupuis Apr 9, 2024
f310269
PHP CS Fixes
adriendupuis Apr 9, 2024
ed6b24a
Update docs/search/activity_log_search_reference/logged_at_criterion.md
adriendupuis Apr 9, 2024
fae5306
…/activity_log_search_reference/: Remove link to private repo
adriendupuis Apr 9, 2024
dd6f60d
Apply suggestions from code review
adriendupuis Apr 9, 2024
6e9192e
Update docs/administration/recent_activity/recent_activity.md
adriendupuis Apr 9, 2024
c300b0f
Apply suggestions from code review
adriendupuis Apr 17, 2024
f4b25ab
Apply suggestions from code review
adriendupuis Apr 18, 2024
0727b5d
Format policies (suggestions from code review)
adriendupuis Apr 18, 2024
6f33bd8
Rename ez*.raml to ibexa*.raml (#2360)
adriendupuis Apr 18, 2024
da07318
Merge branch 'master' into activity-log_toc
adriendupuis Apr 18, 2024
d534f7b
Rebuild rest_api_reference.html
adriendupuis Apr 18, 2024
76b8b05
Fix activity_log_criteria.md table
adriendupuis Apr 18, 2024
600bc31
Merge branch 'master' into activity-log_toc
adriendupuis Apr 19, 2024
256d5e1
Update mkdocs.yml
adriendupuis Apr 19, 2024
1dd5e69
Apply suggestions from code review
adriendupuis Apr 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
adriendupuis marked this conversation as resolved.
Show resolved Hide resolved
{
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 -%}