aboutsummaryrefslogtreecommitdiff
path: root/lib/models/resources/ResourceCategory.php
diff options
context:
space:
mode:
authorPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
committerPhilipp Schüttlöffel <schuettloeffel@zqs.uni-hannover.de>2024-09-24 10:53:31 +0200
commit4459dd7917f4d1c34f40bb68f0e991e9c3d53e4c (patch)
tree5c07151ae61276d334e88f6309c30d439a85c12e /lib/models/resources/ResourceCategory.php
parentda0022e5c1abbf9825ae76debaabdff7e8623bb4 (diff)
parent97a188592c679890a25c37ab78463add76a52ff7 (diff)
Merge branch 'main' into issue-3911issue-3911
Diffstat (limited to 'lib/models/resources/ResourceCategory.php')
-rw-r--r--lib/models/resources/ResourceCategory.php806
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);
+ }
+ }
+}