]> BookStack Code Mirror - bookstack/commitdiff
Added further attribute endpoints and added tests
authorDan Brown <redacted>
Sat, 7 May 2016 13:29:43 +0000 (14:29 +0100)
committerDan Brown <redacted>
Sat, 7 May 2016 13:29:43 +0000 (14:29 +0100)
14 files changed:
app/Entity.php
app/Http/Controllers/AttributeController.php
app/Http/Controllers/Controller.php
app/Http/routes.php
app/Repos/AttributeRepo.php
app/Repos/PageRepo.php
database/factories/ModelFactory.php
tests/Auth/AuthTest.php
tests/Entity/AttributeTests.php [new file with mode: 0644]
tests/Entity/EntityTest.php
tests/Entity/PageDraftTest.php
tests/Permissions/RestrictionsTest.php
tests/TestCase.php
tests/UserProfileTest.php

index abf3e834ede4b129c2e467f55476e28e67fde725..79ff9ff38616ba3b8d849e37d52d958ce9a2b90d 100644 (file)
@@ -1,7 +1,7 @@
 <?php namespace BookStack;
 
 
-abstract class Entity extends Ownable
+class Entity extends Ownable
 {
 
     /**
@@ -200,11 +200,5 @@ abstract class Entity extends Ownable
 
         return $search->orderBy('title_relevance', 'desc');
     }
-
-    /**
-     * Get the url for this item.
-     * @return string
-     */
-    abstract public function getUrl();
-
+    
 }
index 09523af47accd707d2c445d1f0a9c7728e372d55..d7282696aa9a4e293ec7e9d67454d72dc101091a 100644 (file)
@@ -2,7 +2,6 @@
 
 use BookStack\Repos\AttributeRepo;
 use Illuminate\Http\Request;
-
 use BookStack\Http\Requests;
 
 class AttributeController extends Controller
@@ -19,7 +18,6 @@ class AttributeController extends Controller
         $this->attributeRepo = $attributeRepo;
     }
 
-
     /**
      * Get all the Attributes for a particular entity
      * @param $entityType
@@ -27,6 +25,43 @@ class AttributeController extends Controller
      */
     public function getForEntity($entityType, $entityId)
     {
-        
+        $attributes = $this->attributeRepo->getForEntity($entityType, $entityId);
+        return response()->json($attributes);
     }
+
+    /**
+     * Update the attributes for a particular entity.
+     * @param $entityType
+     * @param $entityId
+     * @param Request $request
+     * @return mixed
+     */
+    public function updateForEntity($entityType, $entityId, Request $request)
+    {
+
+        $this->validate($request, [
+            'attributes.*.name' => 'required|min:3|max:250',
+            'attributes.*.value' => 'max:250'
+        ]);
+
+        $entity = $this->attributeRepo->getEntity($entityType, $entityId, 'update');
+        if ($entity === null) return $this->jsonError("Entity not found", 404);
+
+        $inputAttributes = $request->input('attributes');
+        $attributes = $this->attributeRepo->saveAttributesToEntity($entity, $inputAttributes);
+        return response()->json($attributes);
+    }
+
+    /**
+     * Get attribute name suggestions from a given search term.
+     * @param Request $request
+     */
+    public function getNameSuggestions(Request $request)
+    {
+        $searchTerm = $request->get('search');
+        $suggestions = $this->attributeRepo->getNameSuggestions($searchTerm);
+        return response()->json($suggestions);
+    }
+
+
 }
index f0cb47cd9197f6cfaf3b6c3441390e4ecd57be8f..26eeb3002aeebecd9ec74796f428fe9eb2132a5a 100644 (file)
@@ -110,4 +110,15 @@ abstract class Controller extends BaseController
         return true;
     }
 
+    /**
+     * Send back a json error message.
+     * @param string $messageText
+     * @param int $statusCode
+     * @return mixed
+     */
+    protected function jsonError($messageText = "", $statusCode = 500)
+    {
+        return response()->json(['message' => $messageText], $statusCode);
+    }
+
 }
index 8b7ec3bc27166c85fa73b0bc911aa9293fd1e650..7c6911b2e74df0448708066fbf9aa17c40e1ac70 100644 (file)
@@ -88,6 +88,8 @@ Route::group(['middleware' => 'auth'], function () {
     // Attribute routes (AJAX)
     Route::group(['prefix' => 'ajax/attributes'], function() {
         Route::get('/get/{entityType}/{entityId}', 'AttributeController@getForEntity');
+        Route::get('/suggest', 'AttributeController@getNameSuggestions');
+        Route::post('/update/{entityType}/{entityId}', 'AttributeController@updateForEntity');
     });
 
     // Links
index d5cf5b0fdb55049912ce3dafd153d56e97831310..7318b253b8eb5c3f3a085db6ceb138b6c18d6a56 100644 (file)
@@ -28,5 +28,76 @@ class AttributeRepo
         $this->permissionService = $ps;
     }
 
+    /**
+     * Get an entity instance of its particular type.
+     * @param $entityType
+     * @param $entityId
+     * @param string $action
+     */
+    public function getEntity($entityType, $entityId, $action = 'view')
+    {
+        $entityInstance = $this->entity->getEntityInstance($entityType);
+        $searchQuery = $entityInstance->where('id', '=', $entityId)->with('attributes');
+        $searchQuery = $this->permissionService->enforceEntityRestrictions($searchQuery, $action);
+        return $searchQuery->first();
+    }
+
+    /**
+     * Get all attributes for a particular entity.
+     * @param string $entityType
+     * @param int $entityId
+     * @return mixed
+     */
+    public function getForEntity($entityType, $entityId)
+    {
+        $entity = $this->getEntity($entityType, $entityId);
+        if ($entity === null) return collect();
+
+        return $entity->attributes;
+    }
+
+    /**
+     * Get attribute name suggestions from scanning existing attribute names.
+     * @param $searchTerm
+     * @return array
+     */
+    public function getNameSuggestions($searchTerm)
+    {
+        if ($searchTerm === '') return [];
+        $query = $this->attribute->where('name', 'LIKE', $searchTerm . '%')->groupBy('name')->orderBy('name', 'desc');
+        $query = $this->permissionService->filterRestrictedEntityRelations($query, 'attributes', 'entity_id', 'entity_type');
+        return $query->get(['name'])->pluck('name');
+    }
+
+    /**
+     * Save an array of attributes to an entity
+     * @param Entity $entity
+     * @param array $attributes
+     * @return array|\Illuminate\Database\Eloquent\Collection
+     */
+    public function saveAttributesToEntity(Entity $entity, $attributes = [])
+    {
+        $entity->attributes()->delete();
+        $newAttributes = [];
+        foreach ($attributes as $attribute) {
+            $newAttributes[] = $this->newInstanceFromInput($attribute);
+        }
+
+        return $entity->attributes()->saveMany($newAttributes);
+    }
+
+    /**
+     * Create a new Attribute instance from user input.
+     * @param $input
+     * @return static
+     */
+    protected function newInstanceFromInput($input)
+    {
+        $name = trim($input['name']);
+        $value = isset($input['value']) ? trim($input['value']) : '';
+        // Any other modification or cleanup required can go here
+        $values = ['name' => $name, 'value' => $value];
+        return $this->attribute->newInstance($values);
+    }
 
 }
\ No newline at end of file
index 549ec98a7b27595dab12c1cb63a042b1977aeb2a..ef50b7181123a2bd80429646d5e436545e3b0892 100644 (file)
@@ -582,6 +582,7 @@ class PageRepo extends EntityRepo
     {
         Activity::removeEntity($page);
         $page->views()->delete();
+        $page->attributes()->delete();
         $page->revisions()->delete();
         $page->permissions()->delete();
         $this->permissionService->deleteJointPermissionsForEntity($page);
index 2840356e87198633213a700cec46109cdb494f1a..a1a6a92a034be21b50def2896be2cc9d24a3135e 100644 (file)
@@ -52,4 +52,11 @@ $factory->define(BookStack\Role::class, function ($faker) {
         'display_name' => $faker->sentence(3),
         'description' => $faker->sentence(10)
     ];
+});
+
+$factory->define(BookStack\Attribute::class, function ($faker) {
+    return [
+        'name' => $faker->city,
+        'value' => $faker->sentence(3)
+    ];
 });
\ No newline at end of file
index 067840841c8938aed53cba08a2c7c30356ac27f8..306771ed5a98883b4fb87b2242ecd90709a054fc 100644 (file)
@@ -181,7 +181,7 @@ class AuthTest extends TestCase
     public function test_user_deletion()
     {
         $userDetails = factory(\BookStack\User::class)->make();
-        $user = $this->getNewUser($userDetails->toArray());
+        $user = $this->getEditor($userDetails->toArray());
 
         $this->asAdmin()
             ->visit('/settings/users/' . $user->id)
diff --git a/tests/Entity/AttributeTests.php b/tests/Entity/AttributeTests.php
new file mode 100644 (file)
index 0000000..11b66b9
--- /dev/null
@@ -0,0 +1,115 @@
+<?php namespace Entity;
+
+use BookStack\Attribute;
+use BookStack\Page;
+use BookStack\Services\PermissionService;
+
+class AttributeTests extends \TestCase
+{
+
+    protected $defaultAttrCount = 20;
+
+    /**
+     * Get an instance of a page that has many attributes.
+     * @param Attribute[]|bool $attributes
+     * @return mixed
+     */
+    protected function getPageWithAttributes($attributes = false)
+    {
+        $page = Page::first();
+
+        if (!$attributes) {
+            $attributes = factory(Attribute::class, $this->defaultAttrCount)->make();
+        }
+
+        $page->attributes()->saveMany($attributes);
+        return $page;
+    }
+
+    public function test_get_page_attributes()
+    {
+        $page = $this->getPageWithAttributes();
+
+        // Add some other attributes to check they don't interfere
+        factory(Attribute::class, $this->defaultAttrCount)->create();
+
+        $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id)
+            ->shouldReturnJson();
+
+        $json = json_decode($this->response->getContent());
+        $this->assertTrue(count($json) === $this->defaultAttrCount, "Returned JSON item count is not as expected");
+    }
+
+    public function test_attribute_name_suggestions()
+    {
+        // Create some attributes with similar names to test with
+        $attrs = collect();
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'country']));
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'color']));
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'city']));
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'county']));
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'planet']));
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'plans']));
+        $page = $this->getPageWithAttributes($attrs);
+
+        $this->asAdmin()->get('/ajax/attributes/suggest?search=dog')->seeJsonEquals([]);
+        $this->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country', 'county']);
+        $this->get('/ajax/attributes/suggest?search=cou')->seeJsonEquals(['country', 'county']);
+        $this->get('/ajax/attributes/suggest?search=pla')->seeJsonEquals(['planet', 'plans']);
+    }
+
+    public function test_entity_permissions_effect_attribute_suggestions()
+    {
+        $permissionService = $this->app->make(PermissionService::class);
+
+        // Create some attributes with similar names to test with and save to a page
+        $attrs = collect();
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'country']));
+        $attrs = $attrs->merge(factory(Attribute::class, 5)->make(['name' => 'color']));
+        $page = $this->getPageWithAttributes($attrs);
+
+        $this->asAdmin()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']);
+        $this->asEditor()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']);
+
+        // Set restricted permission the page
+        $page->restricted = true;
+        $page->save();
+        $permissionService->buildJointPermissionsForEntity($page);
+
+        $this->asAdmin()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals(['color', 'country']);
+        $this->asEditor()->get('/ajax/attributes/suggest?search=co')->seeJsonEquals([]);
+    }
+
+    public function test_entity_attribute_updating()
+    {
+        $page = $this->getPageWithAttributes();
+
+        $testJsonData = [
+            ['name' => 'color', 'value' => 'red'],
+            ['name' => 'color', 'value' => ' blue '],
+            ['name' => 'city', 'value' => 'London '],
+            ['name' => 'country', 'value' => ' England'],
+        ];
+        $testResponseJsonData = [
+            ['name' => 'color', 'value' => 'red'],
+            ['name' => 'color', 'value' => 'blue'],
+            ['name' => 'city', 'value' => 'London'],
+            ['name' => 'country', 'value' => 'England'],
+        ];
+
+        $this->asAdmin()->json("POST", "/ajax/attributes/update/page/" . $page->id, ['attributes' => $testJsonData]);
+        $this->asAdmin()->get("/ajax/attributes/get/page/" . $page->id);
+        $jsonData = json_decode($this->response->getContent());
+        // Check counts
+        $this->assertTrue(count($jsonData) === count($testJsonData), "The received attribute count is incorrect");
+        // Check data is correct
+        $testDataCorrect = true;
+        foreach ($jsonData as $data) {
+            $testItem = ['name' => $data->name, 'value' => $data->value];
+            if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
+        }
+        $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
+        $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($jsonData)));
+    }
+
+}
index eebb0dc36288bbaf240b0a73a75dabc78e979afc..3bf6a3f2ac76ed88b56739a3459210c367c818ed 100644 (file)
@@ -161,8 +161,8 @@ class EntityTest extends TestCase
     public function test_entities_viewable_after_creator_deletion()
     {
         // Create required assets and revisions
-        $creator = $this->getNewUser();
-        $updater = $this->getNewUser();
+        $creator = $this->getEditor();
+        $updater = $this->getEditor();
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($creator);
         app('BookStack\Repos\UserRepo')->destroy($creator);
@@ -174,8 +174,8 @@ class EntityTest extends TestCase
     public function test_entities_viewable_after_updater_deletion()
     {
         // Create required assets and revisions
-        $creator = $this->getNewUser();
-        $updater = $this->getNewUser();
+        $creator = $this->getEditor();
+        $updater = $this->getEditor();
         $entities = $this->createEntityChainBelongingToUser($creator, $updater);
         $this->actingAs($updater);
         app('BookStack\Repos\UserRepo')->destroy($updater);
@@ -198,7 +198,7 @@ class EntityTest extends TestCase
 
     public function test_recently_created_pages_view()
     {
-        $user = $this->getNewUser();
+        $user = $this->getEditor();
         $content = $this->createEntityChainBelongingToUser($user);
 
         $this->asAdmin()->visit('/pages/recently-created')
@@ -207,7 +207,7 @@ class EntityTest extends TestCase
 
     public function test_recently_updated_pages_view()
     {
-        $user = $this->getNewUser();
+        $user = $this->getEditor();
         $content = $this->createEntityChainBelongingToUser($user);
 
         $this->asAdmin()->visit('/pages/recently-updated')
@@ -241,7 +241,7 @@ class EntityTest extends TestCase
 
     public function test_recently_created_pages_on_home()
     {
-        $entityChain = $this->createEntityChainBelongingToUser($this->getNewUser());
+        $entityChain = $this->createEntityChainBelongingToUser($this->getEditor());
         $this->asAdmin()->visit('/')
             ->seeInElement('#recently-created-pages', $entityChain['page']->name);
     }
index 2c9a288140574e088aaecd3bcf09555c79ac4023..108b7459f79c860e7d12c8d4649b87bea6042ab6 100644 (file)
@@ -32,7 +32,7 @@ class PageDraftTest extends TestCase
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
-        $newUser = $this->getNewUser();
+        $newUser = $this->getEditor();
         $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
         $this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
             ->dontSeeInField('html', $newContent);
@@ -54,7 +54,7 @@ class PageDraftTest extends TestCase
             ->dontSeeInField('html', $addedContent);
 
         $newContent = $this->page->html . $addedContent;
-        $newUser = $this->getNewUser();
+        $newUser = $this->getEditor();
         $this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
 
         $this->actingAs($newUser)
@@ -79,7 +79,7 @@ class PageDraftTest extends TestCase
     {
         $book = \BookStack\Book::first();
         $chapter = $book->chapters->first();
-        $newUser = $this->getNewUser();
+        $newUser = $this->getEditor();
 
         $this->actingAs($newUser)->visit('/')
             ->visit($book->getUrl() . '/page/create')
index 75d83cbfc42a73519eb8c118386a05dcf209eee6..d3830cff773da95d36550fa927981e2679a62bfb 100644 (file)
@@ -9,7 +9,7 @@ class RestrictionsTest extends TestCase
     public function setUp()
     {
         parent::setUp();
-        $this->user = $this->getNewUser();
+        $this->user = $this->getEditor();
         $this->viewer = $this->getViewer();
         $this->restrictionService = $this->app[\BookStack\Services\PermissionService::class];
     }
index 5d0545b6633ba5b459a9c604f2a5781e44f6954b..4c2893f4e7d43ce8f0c85850ba078cce81658bc0 100644 (file)
@@ -14,7 +14,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
      * @var string
      */
     protected $baseUrl = 'http://localhost';
+
+    // Local user instances
     private $admin;
+    private $editor;
 
     /**
      * Creates the application.
@@ -30,6 +33,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
         return $app;
     }
 
+    /**
+     * Set the current user context to be an admin.
+     * @return $this
+     */
     public function asAdmin()
     {
         if($this->admin === null) {
@@ -39,6 +46,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
         return $this->actingAs($this->admin);
     }
 
+    /**
+     * Set the current editor context to be an editor.
+     * @return $this
+     */
+    public function asEditor()
+    {
+        if($this->editor === null) {
+            $this->editor = $this->getEditor();
+        }
+        return $this->actingAs($this->editor);
+    }
+
     /**
      * Quickly sets an array of settings.
      * @param $settingsArray
@@ -79,7 +98,7 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
      * @param array $attributes
      * @return mixed
      */
-    protected function getNewUser($attributes = [])
+    protected function getEditor($attributes = [])
     {
         $user = factory(\BookStack\User::class)->create($attributes);
         $role = \BookStack\Role::getRole('editor');
index 170e7eed17018f41dfd20ca7dd97cf0f9129df59..40ae004e981681b66e6fbea20b891c8a8e5fb248 100644 (file)
@@ -33,7 +33,7 @@ class UserProfileTest extends TestCase
 
     public function test_profile_page_shows_created_content_counts()
     {
-        $newUser = $this->getNewUser();
+        $newUser = $this->getEditor();
 
         $this->asAdmin()->visit('/user/' . $newUser->id)
             ->see($newUser->name)
@@ -52,7 +52,7 @@ class UserProfileTest extends TestCase
 
     public function test_profile_page_shows_recent_activity()
     {
-        $newUser = $this->getNewUser();
+        $newUser = $this->getEditor();
         $this->actingAs($newUser);
         $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
         Activity::add($entities['book'], 'book_update', $entities['book']->id);
@@ -66,7 +66,7 @@ class UserProfileTest extends TestCase
 
     public function test_clicking_user_name_in_activity_leads_to_profile_page()
     {
-        $newUser = $this->getNewUser();
+        $newUser = $this->getEditor();
         $this->actingAs($newUser);
         $entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
         Activity::add($entities['book'], 'book_update', $entities['book']->id);