diff options
Diffstat (limited to 'lib/models/resources/ResourceCategory.php')
| -rw-r--r-- | lib/models/resources/ResourceCategory.php | 806 |
1 files changed, 806 insertions, 0 deletions
diff --git a/lib/models/resources/ResourceCategory.php b/lib/models/resources/ResourceCategory.php new file mode 100644 index 0000000..cfe323a --- /dev/null +++ b/lib/models/resources/ResourceCategory.php @@ -0,0 +1,806 @@ +<?php + +/** + * ResourceCategory.php - model class for resource categories + * + * The ResourceCategory class can be used as a Factory for + * Resource objects. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License, or (at your option) any later version. + * + * @author Moritz Strohm <strohm@data-quest.de> + * @copyright 2017 + * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 + * @category Stud.IP + * @package resources + * @since 4.1 + * + * @property string $id database column + * @property string $name database column + * @property string $description database column + * @property int $system database column + * @property int|null $iconnr database column + * @property string $class_name database column + * @property int $mkdate database column + * @property int $chdate database column + * @property SimpleORMapCollection|ResourceCategoryProperty[] $property_links has_many ResourceCategoryProperty + * @property SimpleORMapCollection|ResourcePropertyDefinition[] $property_definitions has_and_belongs_to_many ResourcePropertyDefinition + */ +class ResourceCategory extends SimpleORMap +{ + private static $cache; + + protected static function configure($config = []) + { + $config['db_table'] = 'resource_categories'; + + $config['has_and_belongs_to_many']['property_definitions'] = [ + 'class_name' => ResourcePropertyDefinition::class, + 'assoc_foreign_key' => 'property_id', + 'thru_table' => 'resource_category_properties', + 'thru_key' => 'category_id', + 'order_by' => 'ORDER BY name ASC' + ]; + + $config['has_many']['property_links'] = [ + 'class_name' => ResourceCategoryProperty::class, + 'assoc_func' => 'findByCategory_id', + 'foreign_key' => 'id', + 'on_delete' => 'delete' + ]; + + $config['registered_callbacks']['after_create'][] = function ($category) { + self::$cache[$category->id] = $category; + }; + $config['registered_callbacks']['after_store'][] = function ($category) { + self::$cache[$category->id] = $category; + }; + $config['registered_callbacks']['after_delete'][] = function ($category) { + unset(self::$cache[$category->id]); + }; + + parent::configure($config); + } + + /** + * Retrieves all resource categories from the database. + * @param bool $force_reload + * @return ResourceCategory[] An array of ResourceCategory objects + * or an empty array if no resource categories are defined. + */ + public static function findAll($force_reload = false) + { + if (!is_array(self::$cache) || $force_reload) { + self::$cache = []; + foreach (self::findBySql('1 ORDER BY name') as $one) { + self::$cache[$one->id] = $one; + } + } + return self::$cache; + } + + public static function find($id) + { + $all = self::findAll(); + return $all[$id] ?? null; + } + + /** + * "Converts" a category-ID to a class name by looking up the + * class name of a specified category. + * + * @param string $category_id The category-ID of the specified category. + * + * @return string The class name field of the category which is specified + * by $category_id. In case no category could be found, an empty + * string is returned. + */ + public static function getClassNameById($category_id) + { + $category = self::find($category_id); + if ($category) { + return $category->class_name; + } + return ''; + } + + public function hasResources() + { + $db = DBManager::get(); + $stmt = $db->prepare( + "SELECT 1 FROM resources WHERE category_id = :category_id LIMIT 1" + ); + $stmt->execute(['category_id' => $this->id]); + return $stmt->fetchColumn() !== false; + } + + /** + * Retrieves the definitions of all properties that are available + * for this resource category. + * + * @param string[] excluded_properties An array with the names + * of the properties that shall be excluded from the result set. + * + * @return ResourcePropertyDefinition[] An array of resource property + * definitions. + */ + public function getPropertyDefinitions($excluded_properties = []) + { + if (is_array($excluded_properties) && count($excluded_properties)) { + return ResourcePropertyDefinition::findBySql( + "INNER JOIN resource_category_properties rcp + ON resource_property_definitions.property_id = rcp.property_id + WHERE + rcp.category_id = :category_id + AND resource_property_definitions.name NOT IN ( + :excluded_properties + ) + ORDER BY resource_property_definitions.name ASC", + [ + 'category_id' => $this->id, + 'excluded_properties' => $excluded_properties + ] + ); + } + + //No excluded properties are specified. + //We can return all property definitions. + return ResourcePropertyDefinition::findBySql( + "INNER JOIN resource_category_properties rcp + ON resource_property_definitions.property_id = rcp.property_id + WHERE + rcp.category_id = :category_id + ORDER BY resource_property_definitions.name ASC", + [ + 'category_id' => $this->id + ] + ); + } + + /** + * This method returns the same properties as getPropertyDefinitions, + * but grouped and ordered by the property groups and the position of the + * property in that group. + * + * @return array An array with the group names as keys and the properties + * in the second array dimension. The structure of the array + * is as follows: + * [ + * group1 name => [ + * property1, + * property2, + * ... + * ], + * group2 name => [ + * ... + * ] + * ] + */ + public function getGroupedPropertyDefinitions($excluded_properties = []) + { + if (is_array($excluded_properties) && count($excluded_properties)) { + $definitions = ResourcePropertyDefinition::findBySql( + "INNER JOIN resource_category_properties rcp + ON resource_property_definitions.property_id = rcp.property_id + LEFT JOIN resource_property_groups rpg + ON resource_property_definitions.property_group_id = rpg.id + WHERE + rcp.category_id = :category_id + AND resource_property_definitions.name NOT IN ( + :excluded_properties + ) + ORDER BY + type DESC, rpg.position ASC, rpg.name ASC, + resource_property_definitions.property_group_pos, + resource_property_definitions.name ASC", + [ + 'category_id' => $this->id, + 'excluded_properties' => $excluded_properties + ] + ); + + $empty_index = _('Sonstige'); + $empty_property_group = [ + $empty_index => [] + ]; + $property_groups = []; + foreach ($definitions as $definition) { + if ($definition->group && $definition->group->name) { + $group_name = $definition->group->name; + if (!is_array($property_groups[$group_name])) { + $property_groups[$group_name] = []; + } + $property_groups[$group_name][] = $definition; + } else { + $empty_property_group[$empty_index][] = $definition; + } + } + if ($empty_property_group[$empty_index]) { + return array_merge($property_groups, $empty_property_group); + } else { + return $property_groups; + } + } + + //No excluded properties are specified. + //We can return all property definitions. + $definitions = ResourcePropertyDefinition::findBySql( + "INNER JOIN resource_category_properties rcp + ON resource_property_definitions.property_id = rcp.property_id + LEFT JOIN resource_property_groups rpg + ON resource_property_definitions.property_group_id = rpg.id + WHERE + rcp.category_id = :category_id + ORDER BY + rpg.position ASC, rpg.name ASC, + resource_property_definitions.property_group_pos, + resource_property_definitions.name ASC", + [ + 'category_id' => $this->id + ] + ); + + $empty_index = _('Sonstige'); + $empty_property_group = [ + $empty_index => [] + ]; + $property_groups = []; + foreach ($definitions as $definition) { + if ($definition->group->name) { + $group_name = $definition->group->name; + if (!is_array($property_groups[$group_name])) { + $property_groups[$group_name] = []; + } + $property_groups[$group_name][] = $definition; + } else { + $empty_property_group[$empty_index][] = $definition; + } + } + if ($empty_property_group[$empty_index]) { + return array_merge($property_groups, $empty_property_group); + } else { + return $property_groups; + } + } + + /** + * Adds a property to this category. If the property doesn't exist + * it will be created. + * + * @param string $name The name of the property. + * @param string $type The type of the property. + * @param bool $requestable Whether the property is requestable or not. + * Defaults to false. + * @param bool $protected Whether the property is protected or not. + * Defaults to false. + * @param string $write_permission_level + * @return ResourceCategoryProperty The created or updated + * resource category property. + * @throws ResourcePropertyDefinitionException If the property definition + * cannot be created. + * + * @throws ResourcePropertyException If the property cannot be created. + */ + public function addProperty( + $name = '', + $type = 'bool', + $requestable = false, + $protected = false, + $write_permission_level = 'autor' + ) + { + if (!$name) { + throw new ResourcePropertyException( + _('Es wurde kein Name für die Eigenschaft angegeben!') + ); + } + + $defined_types = ResourcePropertyDefinition::getDefinedTypes(); + + if (!in_array($type, $defined_types)) { + throw new ResourcePropertyException( + sprintf( + _('Der Eigenschaftstyp %s ist ungültig!'), + $type + ) + ); + } + + if (!in_array($write_permission_level, ['user', 'autor', 'tutor', 'admin'])) { + throw new ResourcePropertyException( + sprintf( + _('Die Rechtestufe %s ist ungültig!'), + $write_permission_level + ) + ); + } + + $existing_property = ResourceCategoryProperty::findOneBySql( + 'INNER JOIN resource_property_definitions rpd + ON resource_category_properties.property_id = rpd.property_id + WHERE + resource_category_properties.category_id = :category_id + AND + rpd.name = :name + AND + rpd.type = :type', + [ + 'category_id' => $this->id, + 'name' => $name, + 'type' => $type + ] + ); + + if ($existing_property) { + $existing_property->requestable = $requestable ? '1' : '0'; + $existing_property->protected = $protected ? '1' : '0'; + if ($existing_property->isDirty() && !$existing_property->store()) { + throw new ResourcePropertyException( + sprintf( + _('Fehler beim Aktualisieren der Eigenschaft %1$s (vom Typ %2$s)!'), + $name, + $type + ) + ); + } + return $existing_property; + } else { + $definition = ResourcePropertyDefinition::findOneBySql( + 'name = :name AND type = :type', + [ + 'name' => $name, + 'type' => $type + ] + ); + + if (!$definition) { + $definition = new ResourcePropertyDefinition(); + $definition->name = $name; + $definition->type = $type; + if (!$definition->store()) { + throw new ResourcePropertyDefinitionException( + sprintf( + _('Fehler beim Speichern der Definition der Eigenschaft %1$s (vom Typ %2$s)!'), + $name, + $type + ) + ); + } + } + + $property = new ResourceCategoryProperty(); + $property->property_id = $definition->id; + $property->category_id = $this->id; + $property->requestable = $requestable ? '1' : '0'; + $property->protected = $protected ? '1' : '0'; + + if ($property->store()) { + return $property; + } else { + throw new ResourcePropertyException( + sprintf( + _('Fehler beim Speichern der neuen Eigenschaft %1$s (vom Typ %2$s)!'), + $name, + $type + ) + ); + } + } + } + + /** + * Creates a resource object which belongs to this category. + * All properties which are mandatory for resources of this + * category are set to default values unless they are specified in the + * properties array. + * + * @param string $name The name of the new resource. + * @param string $description The description of the new resource. + * @param string $parent_id The parent resource's ID (if any). + * @param array $properties An associative array in the form [$key] = $value + * containing the defined properties which can be set to this resource. + * @param bool $ignore_invalid If set to true, invalid values or invalid + * property names are ignored and no exception is thrown if an invalid + * value or property name occurs. Instead an invalid valud will be replaced + * with a default value and an invalid property name will not result + * in a set property. + * + * @return Resource New Resource object which is a member of this resource category. + * @throws InvalidResourceException if the resource cannot be stored. + * @throws ResourcePropertyException If the name of the resource property + * is not defined for this resource category. + * @throws InvalidResourceCategoryException if the class_name attribute of + * the resource category contains a class name of a class which is not + * derived from the Resource class. + */ + public function createResource( + $name = '', + $description = '', + $parent_id = '', + $properties = [], + $ignore_invalid = false + ) + { + if (($this->class_name != 'Resource') and + !is_subclass_of($this->class_name, 'Resource')) { + //Invalid resource category specification: + //All class names for a resource category must be derived from the + //resource class! + throw new InvalidResourceCategoryException( + sprintf( + _('Die Ressourcenkategorie %1$s ist ungültig, da die dort angegebene Klasse %2$s nicht von der Klasse Resource abgeleitet ist!'), + $this->name, + $this->class_name + ) + ); + } + + $resource = new $this->class_name; + $resource->parent_id = $parent_id; + $resource->category_id = $this->id; + $resource->name = $name; + $resource->description = $description; + + if (!$resource->store()) { + throw new InvalidResourceException( + sprintf( + _('Fehler beim Speichern der Resource %1$s!'), + $resource->name + ) + ); + } + + //The resource is stored. We can now store its attributes: + if ($properties) { + foreach ($properties as $name => $state) { + try { + $resource->setProperty($name, $state); + } catch (ResourcePropertyException $e) { + if (!$ignore_invalid) { + throw $e; + } + } + } + } + + return $resource; + } + + /** + * Returns the default state for a resource property. + * Depending on the type of the resource property + * different state is returned. + * + * @param ResourcePropertyDefinition $definition The definition of a + * resource property whose default state shall be returned. + * + * @return mixed The default state for the property type, + * specified by the given property definition. + */ + protected function setPropertyDefaultState( + ResourcePropertyDefinition $definition + ) + { + switch ($definition->type) { + case 'bool': + return false; + case 'num': + return 0; + case 'select': + //Set the first option as default. + //For that, we have to split the option list first, + //since it containts semicolon separated values: + $options = explode(';', $definition->options); + return $options[0]; + case 'user': + //Return the ID of the current user: + return User::findCurrent()->id; + case 'position': + return '+0.0+0.0+0.0CRSWGS_84/'; + case 'institute': + case 'fileref': + case 'url': + case 'text': + return ''; + } + } + + /** + * Creates a ResourceProperty object for a specified Resource object. + * + * @param Resource $resource The resource for which the ResourceProperty + * object shall be built. + * @param string $name The name of the property. + * @param string $state The value of the property. + * + * @return ResourceProperty A ResourceProperty object + * for the given Resource object. + * @throws ResourcePropertyException If the name of the resource property + * is not defined for this resource category. + * + * @throws InvalidResourceCategoryException If this resource category + * doesn't match the category of the resource object. + */ + public function createDefinedResourceProperty(Resource $resource, $name, $state = null) + { + if ($resource->category_id != $this->id) { + throw new InvalidResourceCategoryException( + sprintf( + _('Die Ressource %1$s ist kein Mitglied der Ressourcenkategorie %2$s!'), + $resource->name, + $this->name + ) + ); + } + + if (!$resource->id) { + //The resource has no ID: probably it is a new resource. + if ($resource->isNew()) { + //We need an ID so we have to create one: + $resource->getNewId(); + } else { + throw new InvalidResourceException( + sprintf( + _('Die Ressource %1$s besitzt keine ID!'), + $resource->name + ) + ); + } + } + + + //get property definition: + $definition = ResourcePropertyDefinition::findOneBySql( + 'INNER JOIN resource_category_properties rcp + ON rcp.property_id = resource_property_definitions.property_id + WHERE category_id = :category_id + AND name = :name + LIMIT 1', + [ + 'category_id' => $this->id, + 'name' => $name + ] + ); + + if (!$definition) { + //Property is undefined for this resource category: + throw new ResourcePropertyException( + sprintf( + _('Die Eigenschaft %1$s ist für die Ressourcenkategorie %2$s nicht definiert!'), + $name, + $this->name + ) + ); + } + + $property = new ResourceProperty(); + $property->resource_id = $resource->id; + $property->property_id = $definition->id; + + //if state is not set we can set it to defined default values + //for some state types: + if ($state === null) { + $property->state = self::setPropertyDefaultState($definition); + } else { + $property->state = $state; + } + + return $property; + } + + /** + * Creates a ResourceRequestProperty object + * for a specified ResourceRequest object. + * + * @param ResourceRequest $request The resource request for which the + * ResourceRequestProperty object shall be built. + * @param string $name The name of the property. + * @param string $state The value of the property. + * + * @return ResourceRequestProperty A ResourceProperty object for the + * given Resource object. + * @throws ResourcePropertyException If the name of the resource property is + * not defined for the resource category of the resource request. + * + * @throws InvalidResourceCategoryException If this resource category + * doesn't match the resource category of the resource request object. + */ + public function createDefinedResourceRequestProperty( + ResourceRequest $request, + $name, + $state = null + ) + { + if ($request->category_id != $this->id) { + throw new InvalidResourceCategoryException( + sprintf( + _('Die Resourcenanfrage %1$s ist kein Mitglied der Ressourcenkategorie %2$s!'), + $request->name, + $this->name + ) + ); + } + + if (!$request->id) { + //The request has no ID: probably it is a new resource request. + if ($request->isNew()) { + //We need an ID so we have to create one: + $request->id = $request->getNewId(); + } else { + throw new InvalidResourceRequestException( + sprintf( + _('Die Ressourcenanfrage %1$s besitzt keine ID!'), + $request->name + ) + ); + } + } + + //get property definition: + $definition = ResourcePropertyDefinition::findOneBySql( + 'INNER JOIN resource_category_properties rcp + ON rcp.property_id = resource_property_definitions.property_id + WHERE category_id = :category_id + AND name = :name + LIMIT 1', + [ + 'category_id' => $this->id, + 'name' => $name + ] + ); + + if (!$definition) { + //Property is undefined for this resource category: + throw new ResourcePropertyException( + sprintf( + _('Die Eigenschaft %1$s ist für die Ressourcenkategorie %2$s nicht definiert!'), + $name, + $this->name + ) + ); + } + + $property = new ResourceRequestProperty(); + $property->request_id = $request->id; + $property->property_id = $definition->id; + + //if state is not set we can set it to defined default values + //for some state types: + if ($state === null) { + $property->state = self::setPropertyDefaultState($definition); + } else { + $property->state = $state; + } + + return $property; + } + + public function getRequestableProperties() + { + return ResourcePropertyDefinition::findBySql( + "INNER JOIN resource_category_properties + USING (property_id) + WHERE + resource_category_properties.category_id = :category_id + AND + resource_category_properties.requestable = '1' + ORDER BY type DESC, name ASC", + [ + 'category_id' => $this->id + ] + ); + } + + /** + * Determines if this resource category has a property with the + * specified name and type. + * + * @param string $name The requested property name. + * @param string $type The requested property type (optional). + * + * @return bool True, if a property with the specified name and type + * exists, false otherwise. + */ + public function hasProperty($name = '', $type = null) + { + if ($type) { + return ResourceCategoryProperty::countBySql( + 'INNER JOIN resource_property_definitions + USING (property_id) + WHERE + name = :name + AND + type = :type + AND + category_id = :category_id', + [ + 'name' => $name, + 'type' => $type, + 'category_id' => $this->id + ] + ) > 0; + } else { + return ResourceCategoryProperty::countBySql( + 'INNER JOIN resource_property_definitions + USING (property_id) + WHERE + name = :name + AND + category_id = :category_id', + [ + 'name' => $name, + 'category_id' => $this->id + ] + ) > 0; + } + } + + /** + * Determines if the user has write permissions for the + * resource property specified by its name. + * + * @param string The name of the resource property definition. + * @param User $user The user whose permissions shall be checked. + * @param Resource|null $resource An optional resource that shall be used + * to check for non-global permissions. + * + * @return bool True, if the user has write permissions, false otherwise. + * @throws ResourcePropertyDefinitionException If no property is found. + * + */ + public function userHasPropertyWritePermissions(string $name, User $user, $resource = null) + { + $property = ResourcePropertyDefinition::findOneBySql( + 'name = :name', + [ + 'name' => $name + ] + ); + + if (!$property) { + throw new ResourcePropertyDefinitionException( + sprintf( + _('Die Ressourceneigenschaft %s existiert nicht!'), + $name + ) + ); + } + + if ($property->write_permission_level == 'admin-global') { + return ResourceManager::userHasGlobalPermission( + $user, + 'admin' + ); + } elseif ($resource instanceof Resource) { + //It must be a permission for the specified resource. + return $resource->userHasPermission( + $user, + $property->write_permission_level + ); + } else { + //We cannot check permissions. + return false; + } + } + + /** + * Get the icon of a category + * @param string $role + * @return Icon + */ + public function getIcon($role = Icon::ROLE_INFO) + { + if ($this->iconnr == 0) { + //No special icon + return Icon::create('resources', $role); + } elseif ($this->iconnr == 1) { + return Icon::create('home', $role); + } else { + //No known icon + return Icon::create('resources', $role); + } + } +} |
