partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 3cc8777ab1eb1cf5e07885071b7bea79dd776753
parent 9d3292e3048c87f1281ec8796484e292ddd4cc38
Author: Felicitus <felicitus@felicitus.org>
Date:   Fri, 16 Mar 2012 12:59:26 +0100

Refactored the part manager to the "new" AbstractManager system and added the first unit tests for parts.

Diffstat:
Asrc/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/CategoryNotAssignedException.php | 20++++++++++++++++++++
Asrc/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/StorageLocationNotAssignedException.php | 20++++++++++++++++++++
Msrc/backend/de/RaumZeitLabor/PartKeepr/Part/Part.php | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/backend/de/RaumZeitLabor/PartKeepr/Part/PartManager.php | 159++++++++++++++++++++++++-------------------------------------------------------
Msrc/backend/de/RaumZeitLabor/PartKeepr/Part/PartService.php | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Mtests/Auth/UserTest.php | 8+++-----
Atests/Part/PartServiceTest.php | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atests/Service/ServiceTest.php | 20++++++++++++++++++++
Mtests/bootstrap.php | 8++++----
9 files changed, 354 insertions(+), 175 deletions(-)

diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/CategoryNotAssignedException.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/CategoryNotAssignedException.php @@ -0,0 +1,19 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Part\Exceptions; + +use de\RaumZeitLabor\PartKeepr\PartKeepr, + de\RaumZeitLabor\PartKeepr\Util\SerializableException; + +/** + * This exception is thrown when a part hasn't got a category assigned + */ +class CategoryNotAssignedException extends SerializableException { + + /** + * Constructs the exception + * @param BaseEntity $entity + */ + public function __construct () { + parent::__construct(PartKeepr::i18n("No category assigned")); + } +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/StorageLocationNotAssignedException.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/StorageLocationNotAssignedException.php @@ -0,0 +1,19 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Part\Exceptions; + +use de\RaumZeitLabor\PartKeepr\PartKeepr, + de\RaumZeitLabor\PartKeepr\Util\SerializableException; + +/** + * This exception is thrown when a part hasn't got a storage location assigned + */ +class StorageLocationNotAssignedException extends SerializableException { + + /** + * Constructs the exception + * @param BaseEntity $entity + */ + public function __construct () { + parent::__construct(PartKeepr::i18n("No storage location assigned")); + } +}+ \ No newline at end of file diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/Part.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/Part.php @@ -9,12 +9,16 @@ use de\RaumZeitLabor\PartKeepr\StorageLocation\StorageLocation, de\RaumZeitLabor\PartKeepr\Util\Serializable, de\RaumZeitLabor\PartKeepr\Util\BaseEntity, de\RaumZeitLabor\PartKeepr\PartKeepr, - de\RaumZeitLabor\PartKeepr\Util\Exceptions\OutOfRangeException; + de\RaumZeitLabor\PartKeepr\Part\Exceptions\CategoryNotAssignedException, + de\RaumZeitLabor\PartKeepr\Util\Exceptions\OutOfRangeException, + de\RaumZeitLabor\PartKeepr\Part\Exceptions\StorageLocationNotAssignedException; /** * Represents a part in the database. The heart of our project. Handle with care! - * @Entity **/ + * + * @Entity @HasLifecycleCallbacks + */ class Part extends BaseEntity implements Serializable, Deserializable { /** * The category of the part @@ -530,4 +534,53 @@ class Part extends BaseEntity implements Serializable, Deserializable { } } } + + /** + * Checks if the part category is set. + * + * @throws CategoryNotAssignedException + */ + private function checkCategoryConsistency () { + if ($this->getCategory() === null) { + throw new CategoryNotAssignedException(); + } + } + + /** + * Checks if the part storage location is set. + * + * @throws StorageLocationNotAssignedException + */ + private function checkStorageLocationConsistency () { + if ($this->getStorageLocation() === null) { + throw new StorageLocationNotAssignedException(); + } + } + + /** + * Checks if the requirements for database persisting are given. + * + * @throws CategoryNotAssignedException Thrown if no category is set + * @throws StorageLocationNotAssignedException Thrown if no storage location is set + * + * @PrePersist + */ + public function onPrePersist () { + $this->checkCategoryConsistency(); + $this->checkStorageLocationConsistency(); + } + + /** + * + * Checks if the requirements for database persisting are given. + * + * For a list of exceptions, see + * @see de\RaumZeitLabor\PartKeepr\Part.Part::onPrePersist() + * + * @PreUpdate */ + public function onPreUpdate () { + $this->checkCategoryConsistency(); + $this->checkStorageLocationConsistency(); + } + } diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartManager.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartManager.php @@ -2,7 +2,10 @@ namespace de\RaumZeitLabor\PartKeepr\Part; use de\RaumZeitLabor\PartKeepr\UploadedFile\TempUploadedFile, + de\RaumZeitLabor\PartKeepr\Manager\ManagerFilter, + Doctrine\ORM\QueryBuilder, de\RaumZeitLabor\PartKeepr\PartParameter\PartParameter, + de\RaumZeitLabor\PartKeepr\Manager\AbstractManager, de\RaumZeitLabor\PartKeepr\Unit\Unit, de\RaumZeitLabor\PartKeepr\SiPrefix\SiPrefix, de\RaumZeitLabor\PartKeepr\Part\PartDistributor, @@ -22,120 +25,52 @@ use de\RaumZeitLabor\PartKeepr\UploadedFile\TempUploadedFile, de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager, de\RaumZeitLabor\PartKeepr\Manufacturer\ManufacturerManager; -class PartManager extends Singleton { +class PartManager extends AbstractManager { + + /** + * Returns the FQCN for the target entity to operate on. + * @return string The FQCN, e.g. de\RaumZeitLabor\PartKeepr\Part + */ + public function getEntityName () { + return 'de\RaumZeitLabor\PartKeepr\Part\Part'; + } + + /** + * Returns all fields which need to appear in the getList ResultSet. + * @return array An array of all fields which should be returned + */ + public function getQueryFields () { + return array("name", "averagePrice", "status", "needsReview", "createDate", "id", "stockLevel", + "minStockLevel", "comment", "st.id AS storageLocation_id", "categoryPath", + "st.name as storageLocationName", "f.id AS footprint_id", "f.name AS footprintName", + "c.id AS category", "c.name AS categoryName", "pu.id AS partUnit", "pu.name AS partUnitName", + "pu.is_default AS partUnitDefault" + ); + } + + /** + * Returns the default sort field + * + * @return string The default sort field + */ + public function getDefaultSortField () { + return "dateTime"; + } + /** - * Returns a list of parts. - * - * @todo The parameter list. We need to invent something so that we don't have like 20 parameters for this method. + * Appends various join tables to the result set + * + * (non-PHPdoc) + * @see de\RaumZeitLabor\PartKeepr\Manager.AbstractManager::applyCustomQuery() */ - public function getParts ($start = 0, $limit = 10, $sort = null, $filter = "", $category = 0, $categoryScope = "all", $stockMode = "all", $withoutPrice = false, $storageLocation = "") { - - $qb = PartKeepr::getEM()->createQueryBuilder(); - $qb->select("COUNT(p.id)")->from("de\RaumZeitLabor\PartKeepr\Part\Part","p") - ->join("p.storageLocation", "st") - ->leftJoin("p.footprint", "f") - ->join("p.category", "c") - ->leftJoin("p.partUnit", "pu"); - - - $qb->where("1=1"); - if ($filter != "") { - $qb = $qb->where("LOWER(p.name) LIKE :filter"); - $qb->setParameter("filter", "%".strtolower($filter)."%"); - } - - if ($storageLocation !== null) { - /* If storage location is empty, assume new record. This isn't nice and to be considered as a hack */ - if ($storageLocation == "") { - return array(); - } - $qb->andWhere("st.name = :storageLocation"); - $qb->setParameter("storageLocation", $storageLocation); - } - - $category = intval($category); - - - - if ($category !== 0) { - /* Fetch all children */ - if ($categoryScope == "selected") { - $qb->andWhere("p.category = :category"); - $qb->setParameter("category", $category); - } else { - $childs = PartCategoryManager::getInstance()->getChildNodes($category); - $childs[] = $category; - $qb->andWhere("p.category IN (".implode(",", $childs).")"); - } - } - - switch ($stockMode) { - case "all": - break; - case "zero": - $qb->andWhere("p.stockLevel = 0"); - break; - case "nonzero": - $qb->andWhere("p.stockLevel > 0"); - break; - case "below": - $qb->andWhere("p.stockLevel < p.minStockLevel"); - break; - } - - if ($withoutPrice === true || $withoutPrice === "true") { - $qb->andWhere("p.averagePrice IS NULL"); - } - - $totalQuery = $qb->getQuery(); - - - - - $qb->select("p.averagePrice, p.status, p.name, p.needsReview, p.createDate, p.id, p.stockLevel, p.minStockLevel, p.comment, st.id AS storageLocation_id, p.categoryPath, st.name as storageLocationName, f.id AS footprint_id, f.name AS footprintName, c.id AS category, c.name AS categoryName, pu.id AS partUnit, pu.name AS partUnitName, pu.is_default AS partUnitDefault"); - if ($sort === null) { - $qb->addOrderBy("p.name", "ASC"); - } else { - $sortArray = json_decode($sort, true); - - foreach ($sortArray as $sortParam) { - switch ($sortParam["property"]) { - case "storageLocationName": - $orderBy = "st.name"; - break; - case "footprintName": - $orderBy = "f.name"; - break; - default; - $orderBy = "p.".$sortParam["property"]; - break; - } - - $qb->addOrderBy($orderBy, $sortParam["direction"]); - } - } - - - if ($limit > -1) { - $qb->setMaxResults($limit); - $qb->setFirstResult($start); - } - - $query = $qb->getQuery(); - - $result = $query->getArrayResult(); - - foreach ($result as $key => $item) { - $dql = "SELECT COUNT(pa) FROM de\RaumZeitLabor\PartKeepr\Part\PartAttachment pa WHERE pa.part = :part"; - $query = PartKeepr::getEM()->createQuery($dql); - $query->setParameter("part", $item["id"]); - - $result[$key]["attachmentCount"] = $query->getSingleScalarResult(); - } - - - - return array("data" => $result, "totalCount" => $totalQuery->getSingleScalarResult()); + protected function applyCustomQuery (QueryBuilder $qb, ManagerFilter $filter) { + /** + * Pull in additional tables + */ + $qb ->join("q.storageLocation", "st") + ->leftJoin("q.footprint", "f") + ->join("q.category", "c") + ->leftJoin("q.partUnit", "pu"); } public function addOrUpdatePart ($aParameters) { diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartService.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartService.php @@ -4,10 +4,12 @@ namespace de\RaumZeitLabor\PartKeepr\Part; use de\RaumZeitLabor\PartKeepr\User\User, de\RaumZeitLabor\PartKeepr\Service\RestfulService, de\RaumZeitLabor\PartKeepr\Service\Service, + de\RaumZeitLabor\PartKeepr\Manager\ManagerFilter, de\RaumZeitLabor\PartKeepr\Part\PartManager, de\RaumZeitLabor\PartKeepr\Stock\StockEntry, de\RaumZeitLabor\PartKeepr\PartKeepr, de\RaumZeitLabor\PartKeepr\PartCategory\PartCategory, + de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager, de\RaumZeitLabor\PartKeepr\Session\SessionManager; class PartService extends Service implements RestfulService { @@ -15,60 +17,113 @@ class PartService extends Service implements RestfulService { if ($this->hasParameter("id")) { return array("data" => PartManager::getInstance()->getPart($this->getParameter("id"))->serialize()); } else { - return PartManager::getInstance()->getParts( - $this->getParameter("start", $this->getParameter("start", 0)), - $this->getParameter("limit", $this->getParameter("limit", 25)), - $this->getParameter("sort", $this->getParameter("sort")), - $this->getParameter("query", ""), - $this->getParameter("category", 0), - $this->getParameter("categoryScope", "all"), - $this->getParameter("stockMode", "all"), - $this->getParameter("withoutPrice", false), - $this->getParameter("storageLocation", null)); + + $filter = new ManagerFilter($this); + $filter->setFilterCallback(array($this, "filterCallback")); + + return PartManager::getInstance()->getList($filter); } } + /** + * Advanced filtering for the list + * @param QueryBuilder The $queryBuilder + */ + public function filterCallback ($queryBuilder) { + + /** + * Applies text-based filtering + */ + if ($this->hasParameter("query") && $this->getParameter("query") != "") { + $queryBuilder->where("LOWER(q.name) LIKE :filter"); + $queryBuilder->setParameter("filter", "%".strtolower($this->getParameter("query"))."%"); + } + + /** + * Applies filtering by the storage location name + */ + if ($this->getParameter("storageLocation") !== null) { + $queryBuilder->andWhere("st.name = :storageLocation"); + $queryBuilder->setParameter("storageLocation", $this->getParameter("storageLocation")); + } + + /** + * Filter by the category id and set the category mode + * + */ + $category = intval($this->getParameter("category", 0)); + + if ($category !== 0) { + /* Fetch all children */ + if ($this->getParameter("categoryScope") == "selected") { + $queryBuilder->andWhere("q.category = :category"); + $queryBuilder->setParameter("category", $category); + } else { + $childs = PartCategoryManager::getInstance()->getChildNodes($category); + $childs[] = $category; + $queryBuilder->andWhere("q.category IN (".implode(",", $childs).")"); + } + } + + /** + * Filter by the stock mode + */ + switch ($this->getParameter("stockMode")) { + case "all": + break; + case "zero": + $queryBuilder->andWhere("q.stockLevel = 0"); + break; + case "nonzero": + $queryBuilder->andWhere("q.stockLevel > 0"); + break; + case "below": + $queryBuilder->andWhere("q.stockLevel < q.minStockLevel"); + break; + } + + /** + * Filter by the price + */ + if ($this->getParameter("withoutPrice") === true || $this->getParameter("withoutPrice") === "true") { + $queryBuilder->andWhere("q.averagePrice IS NULL"); + } + } + /** * (non-PHPdoc) * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::create() */ public function create () { - $part = new Part(); - $part->deserialize($this->getParameters()); - - PartKeepr::getEM()->persist($part); - PartKeepr::getEM()->flush(); - - if ($this->getParameter("initialStockLevel") > 0) { - - try { - $user = User::loadById($this->getParameter("initialStockLevelUser")); - } catch (\Exception $e) { - $user = SessionManager::getCurrentSession()->getUser(); - } - - $stock = new StockEntry($part, intval($this->getParameter("initialStockLevel")), $user); - - if ($this->getParameter("initialStockLevelPricePerItem") == true) { - $price = floatval($this->getParameter("initialStockLevelPrice")); - } else { - $price = floatval($this->getParameter("initialStockLevelPrice")) / $this->getParameter("initialStockLevel"); - } - - if ($price != 0) { - $stock->setPrice($price); - } - - PartKeepr::getEM()->persist($stock); - PartKeepr::getEM()->flush(); - - $part->updateStockLevel(); - PartKeepr::getEM()->flush(); - - return array("data" => $part->serialize()); + $entity = PartManager::getInstance()->createEntity($this->getParameters()); + + if ($this->getParameter("initialStockLevel") > 0) { + try { + $user = User::loadById($this->getParameter("initialStockLevelUser")); + } catch (\Exception $e) { + $user = SessionManager::getCurrentSession()->getUser(); + } + + $stock = new StockEntry($entity, intval($this->getParameter("initialStockLevel")), $user); + + if ($this->getParameter("initialStockLevelPricePerItem") == true) { + $price = floatval($this->getParameter("initialStockLevelPrice")); + } else { + $price = floatval($this->getParameter("initialStockLevelPrice")) / $this->getParameter("initialStockLevel"); + } + + if ($price != 0) { + $stock->setPrice($price); + } + + PartKeepr::getEM()->persist($stock); + PartKeepr::getEM()->flush(); + + $part->updateStockLevel(); + PartKeepr::getEM()->flush(); } - return array("data" => $part->serialize()); + return array("data" => $entity->serialize()); } /** @@ -76,13 +131,12 @@ class PartService extends Service implements RestfulService { * @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::update() */ public function update () { - $this->requireParameter("id"); - $part = Part::loadById($this->getParameter("id")); - $part->deserialize($this->getParameters()); - - PartKeepr::getEM()->flush(); - - return array("data" => $part->serialize()); + $entity = PartManager::getInstance()->getEntity($this->getParameter("id")); + $entity->deserialize($this->getParameters()); + + PartKeepr::getEM()->flush(); + + return array("data" => $entity->serialize()); } @@ -116,7 +170,6 @@ class PartService extends Service implements RestfulService { } - PartKeepr::getEM()->flush(); } diff --git a/tests/Auth/UserTest.php b/tests/Auth/UserTest.php @@ -2,7 +2,7 @@ namespace de\RaumZeitLabor\PartKeepr\Tests\Auth; declare(encoding = 'UTF-8'); -use de\RaumZeitLabor\PartKeepr\Auth\User; +use de\RaumZeitLabor\PartKeepr\User\User; class UserTest extends \PHPUnit_Framework_TestCase { public function testBasics () { @@ -13,11 +13,11 @@ class UserTest extends \PHPUnit_Framework_TestCase { $user->setUsername("Timo A. Hummel"); - $hashedPassword = "3858f62230ac3c915f300c664312c63f"; - $this->assertEquals($user->getUsername(), "timo_a_hummel"); $user->setPassword("foobar"); + $hashedPassword = "3858f62230ac3c915f300c664312c63f"; + $this->assertEquals($user->comparePassword("foobar"), true, "Error comparing passwords: PasswordTest01"); $this->assertEquals($user->compareHashedPassword($hashedPassword), true, "Error comparing passwords: PasswordTest02"); @@ -26,4 +26,3 @@ class UserTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($user->compareHashedPassword($hashedPassword), true, "Error comparing passwords: PasswordTest04"); } } -?>- \ No newline at end of file diff --git a/tests/Part/PartServiceTest.php b/tests/Part/PartServiceTest.php @@ -0,0 +1,80 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Tests\Part; + +use de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager, + de\RaumZeitLabor\PartKeepr\Part\PartService, + de\RaumZeitLabor\PartKeepr\PartKeepr, + de\RaumZeitLabor\PartKeepr\Part\Part, + de\RaumZeitLabor\PartKeepr\StorageLocation\StorageLocationManager, + de\RaumZeitLabor\PartKeepr\StorageLocation\StorageLocationService; + +class PartServiceTest extends \PHPUnit_Framework_TestCase { + protected $backupGlobals = false; + + private static $storageLocation; + + public static function setUpBeforeClass () { + // Create a storage location for testing + $storageLocation = array("name" => "PartServiceTest"); + + $service = new StorageLocationService($storageLocation); + $result = $service->create(); + + self::$storageLocation = $result["data"]["id"]; + } + + /** + * @expectedException de\RaumZeitLabor\PartKeepr\Part\Exceptions\CategoryNotAssignedException + */ + public function testCreatePartWithoutCategory () { + $partName = "testCreatePartWithoutCategory"; + + $part = array( + "name" => $partName, + "storageLocation" => self::$storageLocation + ); + + $service = new PartService($part); + $service->create(); + } + + /** + * @expectedException de\RaumZeitLabor\PartKeepr\Part\Exceptions\StorageLocationNotAssignedException + */ + public function testCreatePartWithoutStorageLocation () { + $partName = "testCreatePartWithoutStorageLocation"; + + $part = array( + "name" => $partName, + "category" => 1 + ); + + $service = new PartService($part); + $service->create(); + } + + /** + * Verifies that querying for a part works correctly. + * + * There was a bug where only lower-case strings matched against parts, fixed 2012-03-16. + */ + public function testPartNameQuerying () { + $partName = "testPartNameQuerying"; + + + $part = new Part(); + $part->setName($partName); + $part->setCategory(PartCategoryManager::getInstance()->getRootNode()->getNode()); + $part->setStorageLocation(StorageLocationManager::getInstance()->getStorageLocation(self::$storageLocation)); + + PartKeepr::getEM()->persist($part); + PartKeepr::getEM()->flush(); + + $service = new PartService(array("query" => $partName)); + + $response = $service->get(); + + $this->assertEquals(1, $response["totalCount"], "The resultset totalCount is wrong."); + $this->assertEquals($response["data"][0]["name"], $part->getName()); + } +} diff --git a/tests/Service/ServiceTest.php b/tests/Service/ServiceTest.php @@ -0,0 +1,20 @@ +<?php +namespace de\RaumZeitLabor\PartKeepr\Tests\Service; + +use de\RaumZeitLabor\PartKeepr\Service\Service, + de\RaumZeitLabor\PartKeepr\PartKeepr, + de\RaumZeitLabor\PartKeepr\Part\Part; + + +class ServiceTest extends \PHPUnit_Framework_TestCase { + protected $backupGlobals = false; + + /** + * Tests injection of service parameters via the constructor. + */ + public function testConstructorParameterPassing () { + $service = new Service(array("foo" => "bar")); + + $this->assertEquals($service->getParameter("foo"), "bar"); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php @@ -1,8 +1,8 @@ <?php namespace de\RaumZeitLabor\PartKeepr\Tests; -declare(encoding = 'UTF-8'); -use de\raumzeitlabor\PartKeepr\PartKeepr; +use de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager, + de\raumzeitlabor\PartKeepr\PartKeepr; include(dirname(__DIR__). "/src/backend/de/RaumZeitLabor/PartKeepr/PartKeepr.php"); @@ -14,4 +14,4 @@ $classes = PartKeepr::getClassMetaData(); $tool->dropSchema($classes); $tool->createSchema($classes); -?>- \ No newline at end of file +PartCategoryManager::getInstance()->ensureRootExists();+ \ No newline at end of file