Skip to content

Commit

Permalink
Introducing a new property of the user entity for custom structured U…
Browse files Browse the repository at this point in the history
…I data and an endpoint for setting them.
  • Loading branch information
Martin Krulis committed Jun 12, 2019
1 parent e9584cb commit 1af2dc3
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 14 deletions.
37 changes: 37 additions & 0 deletions app/V1Module/presenters/UsersPresenter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Model\Entity\Group;
use App\Model\Entity\Login;
use App\Model\Entity\User;
use App\Model\Entity\UserUiData;
use App\Model\Repository\Logins;
use App\Exceptions\BadRequestException;
use App\Helpers\EmailVerificationHelper;
Expand Down Expand Up @@ -403,6 +404,42 @@ public function actionUpdateSettings(string $id) {
$this->sendSuccessResponse($this->userViewFactory->getUser($user));
}

public function checkUpdateUiData(string $id) {
$user = $this->users->findOrThrow($id);

if (!$this->userAcl->canUpdateProfile($user)) {
throw new ForbiddenRequestException();
}
}

/**
* Update the user-specific structured UI data
* @POST
* @param string $id Identifier of the user
* @Param(type="post", name="uiData", validation="array|null", description="Structured user-specific UI data")
* @throws NotFoundException
*/
public function actionUpdateUiData(string $id) {
$req = $this->getRequest();
$user = $this->users->findOrThrow($id);

$newUiData = $req->getPost("uiData");
if ($newUiData) {
$uiData = $user->getUiData();
if (!$uiData) {
$uiData = new UserUiData($newUiData);
$user->setUiData($uiData);
} else {
$uiData->setData($newUiData);
}
} else {
$user->setUiData(null);
}

$this->users->persist($user);
$this->sendSuccessResponse($this->userViewFactory->getUser($user));
}

public function checkCreateLocalAccount(string $id) {
$user = $this->users->findOrThrow($id);
if (!$this->userAcl->canCreateLocalAccount($user)) {
Expand Down
1 change: 1 addition & 0 deletions app/V1Module/router/RouterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ private static function createUsersRoutes(string $prefix): RouteList {
$router[] = new GetRoute("$prefix/<id>/exercises", "Users:exercises");
$router[] = new PostRoute("$prefix/<id>", "Users:updateProfile");
$router[] = new PostRoute("$prefix/<id>/settings", "Users:updateSettings");
$router[] = new PostRoute("$prefix/<id>/ui-data", "Users:updateUiData");
$router[] = new PostRoute("$prefix/<id>/create-local", "Users:createLocalAccount");
$router[] = new PostRoute("$prefix/<id>/role", "Users:setRole");
$router[] = new PostRoute("$prefix/<id>/allowed", "Users:setAllowed");
Expand Down
20 changes: 20 additions & 0 deletions app/model/entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,24 @@ public function getCreatedAt(): DateTime {
public function updateLastAuthenticationAt() {
$this->lastAuthenticationAt = new DateTime();
}

/**
* @ORM\OneToOne(targetEntity="UserUiData", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $uiData = null;

/**
* @return UserUiData|null
*/
public function getUiData(): ?UserUiData {
return $this->uiData;
}

/**
* @param UserUiData|null
*/
public function setUiData(?UserUiData $uiData) {
$this->uiData = $uiData;
}

}
45 changes: 45 additions & 0 deletions app/model/entity/UserUiData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Model\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* Entity holding user-specific data stored by the UI.
*/
class UserUiData
{
public function __construct($data) {
$this->setData($data);
}

/**
* @ORM\Id
* @ORM\Column(type="guid")
* @ORM\GeneratedValue(strategy="UUID")
*/
protected $id;

/**
* Arbitrary user-specific JSON-structured data stored by the UI.
* @ORM\Column(type="text")
*/
protected $data;

/**
* Get parsed UI data structured as (nested) assoc array.
* @return array
*/
public function getData(): array {
return json_decode($this->data, true);
}

/**
* Set (overwrite) the UI data.
* @param array|object $root Root of the structured data (must be JSON serializable)
*/
public function setData($root) {
$this->data = json_encode($root);
}
}
19 changes: 14 additions & 5 deletions app/model/view/UserViewFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ private function getExternalIds(User $user, bool $canViewAllExternalIds = false)
return $user->getConsolidatedExternalLogins($filter);
}

private function isUserLoggedInUser(User $user) {
return $this->loggedInUser && $this->loggedInUser->getId() === $user->getId();
}

/**
* @param User $user
* @param bool $canViewPrivate
* @param bool $canViewAllExternalIds
* @param bool $reallyShowEverything
* @return array
*/
private function getUserData(User $user, bool $canViewPrivate, bool $canViewAllExternalIds = false) {
private function getUserData(User $user, bool $canViewPrivate, bool $reallyShowEverything = false) {
$privateData = null;
if ($canViewPrivate) {
$login = $this->logins->findByUserId($user->getId());
Expand All @@ -84,13 +88,18 @@ private function getUserData(User $user, bool $canViewPrivate, bool $canViewAllE
"studentOf" => $studentOf->map(function (Group $group) { return $group->getId(); })->getValues(),
"supervisorOf" => $supervisorOf->map(function (Group $group) { return $group->getId(); })->getValues()
],
"settings" => $user->getSettings(),
"emptyLocalPassword" => $emptyLocalPassword,
"isLocal" => $user->hasLocalAccount(),
"isExternal" => $user->hasExternalAccounts(),
"isAllowed" => $user->isAllowed(),
"externalIds" => $this->getExternalIds($user, $canViewAllExternalIds),
"externalIds" => $this->getExternalIds($user, $reallyShowEverything),
];

if ($reallyShowEverything || $this->isUserLoggedInUser($user)) {
$uiData = $user->getUiData();
$privateData["uiData"] = $uiData ? $uiData->getData() : null;
$privateData["settings"] = $user->getSettings();
}
}

return [
Expand All @@ -109,7 +118,7 @@ private function getUserData(User $user, bool $canViewPrivate, bool $canViewAllE
* @return array
*/
public function getFullUser(User $user) {
return $this->getUserData($user, true, true);
return $this->getUserData($user, true, true); // true, true = really show everyting
}

/**
Expand Down
41 changes: 41 additions & 0 deletions migrations/Version20190607231836.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20190607231836 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}

public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

$this->addSql('CREATE TABLE user_ui_data (id CHAR(36) NOT NULL COMMENT \'(DC2Type:guid)\', data LONGTEXT NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE user ADD ui_data_id CHAR(36) DEFAULT NULL COMMENT \'(DC2Type:guid)\'');
$this->addSql('ALTER TABLE user ADD CONSTRAINT FK_8D93D64925B58E7D FOREIGN KEY (ui_data_id) REFERENCES user_ui_data (id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_8D93D64925B58E7D ON user (ui_data_id)');
}

public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

$this->addSql('ALTER TABLE user DROP FOREIGN KEY FK_8D93D64925B58E7D');
$this->addSql('DROP INDEX UNIQ_8D93D64925B58E7D ON user');
$this->addSql('ALTER TABLE user DROP ui_data_id');
$this->addSql('DROP TABLE user_ui_data');
}
}
59 changes: 50 additions & 9 deletions tests/Presenters/UsersPresenter.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use App\Model\Entity\User;
use App\Model\Repository\Logins;
use App\Model\Repository\ExternalLogins;
use App\Model\Repository\Users;
use App\Model\View\UserViewFactory;
use App\Security\Roles;
use App\V1Module\Presenters\UsersPresenter;
use Tester\Assert;
Expand Down Expand Up @@ -77,7 +78,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testGetAllUsers()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);

$request = new Nette\Application\Request($this->presenterPath, 'GET', ['action' => 'default']);
$response = $this->presenter->run($request);
Expand Down Expand Up @@ -188,7 +189,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testDetail()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);
$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);

$request = new Nette\Application\Request($this->presenterPath, 'GET',
Expand All @@ -205,7 +206,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testUpdateProfileWithoutEmailAndPassword()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);
$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);

$firstName = "firstNameUpdated";
Expand Down Expand Up @@ -239,7 +240,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testUpdateProfileWithEmailAndWithoutPassword()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);
$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);

$firstName = "firstNameUpdated";
Expand Down Expand Up @@ -280,7 +281,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testUpdateProfileWithoutEmailAndWithPassword()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);
$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);
$login = $this->presenter->logins->findByUsernameOrThrow($user->getEmail());

Expand Down Expand Up @@ -396,7 +397,16 @@ class TestUsersPresenter extends Tester\TestCase

public function testUpdateSettings()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);

// Testing hack
// User view factory remembers logged in user (so we need to reset it after login)
$this->presenter->userViewFactory = new UserViewFactory(
$this->container->getByType(\App\Security\ACL\IUserPermissions::class),
$this->container->getByType(\App\Model\Repository\Logins::class),
$this->container->getByType(\Nette\Security\User::class)
);

$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);

$darkTheme = false;
Expand Down Expand Up @@ -433,6 +443,37 @@ class TestUsersPresenter extends Tester\TestCase
Assert::equal($submissionEvaluatedEmails, $settings->getSubmissionEvaluatedEmails());
}

public function testUpdateUiData()
{
PresenterTestHelper::loginDefaultAdmin($this->container);

// Testing hack
// User view factory remembers logged in user (so we need to reset it after login)
$this->presenter->userViewFactory = new UserViewFactory(
$this->container->getByType(\App\Security\ACL\IUserPermissions::class),
$this->container->getByType(\App\Model\Repository\Logins::class),
$this->container->getByType(\Nette\Security\User::class)
);

$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);
$uiData = [
'lastSelectedGroup' => '1234',
'stretcherSize' => 42,
'nestedStructure' => [
'pos1' => 19,
'pos2' => 33,
'open' => false,
],
];

$payload = PresenterTestHelper::performPresenterRequest($this->presenter, $this->presenterPath, 'POST',
[ 'action' => 'updateUiData', 'id' => $user->getId() ],
[ 'uiData' => $uiData ]
);

Assert::equal($uiData, $payload["privateData"]["uiData"]);
}

public function testCreateLocalAccount()
{
$instance = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN)->getInstances()->first();
Expand Down Expand Up @@ -490,7 +531,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testStudentGroups()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);
$user = $this->users->getByEmail(PresenterTestHelper::STUDENT_GROUP_MEMBER_LOGIN);

$request = new Nette\Application\Request($this->presenterPath, 'GET',
Expand Down Expand Up @@ -526,7 +567,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testInstances()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);
$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);

$request = new Nette\Application\Request($this->presenterPath, 'GET',
Expand All @@ -547,7 +588,7 @@ class TestUsersPresenter extends Tester\TestCase

public function testExercises()
{
$token = PresenterTestHelper::loginDefaultAdmin($this->container);
PresenterTestHelper::loginDefaultAdmin($this->container);
$user = $this->users->getByEmail(PresenterTestHelper::ADMIN_LOGIN);

$request = new Nette\Application\Request($this->presenterPath, 'GET',
Expand Down

0 comments on commit 1af2dc3

Please sign in to comment.