Skip to content
Snippets Groups Projects
StudipStudyArea.class.php 16.8 KiB
Newer Older
<?php
/**
 * Studienbereich... TODO
 *
 * Copyright (C) 2008 - Marcus Lunzenauer <mlunzena@uos.de>
 *
 * 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.
 *
 * @package     studip
 *
 * @author    mlunzena
 * @author    André Noack <noack@data-quest.de>
 * @copyright (c) Authors
 *
 * @property string sem_tree_id database column
 * @property string id alias column for sem_tree_id
 * @property string parent_id database column
 * @property string priority database column
 * @property string info database column
 * @property string name database column
 * @property string type database column
 * @property SimpleORMapCollection _children has_many StudipStudyArea
 * @property Institute institute belongs_to Institute
 * @property StudipStudyArea _parent belongs_to StudipStudyArea
 * @property SimpleORMapCollection courses has_and_belongs_to_many Course
 */

class StudipStudyArea extends SimpleORMap implements StudipTreeNode
    use StudipTreeNodeCachableTrait;

    /**
     * This constant represents the key of the root area.
     */
    const ROOT = 'root';

    protected static function configure($config = [])
    {
        $config['db_table'] = 'sem_tree';
        $config['has_many']['_children'] = [
            'class_name' => StudipStudyArea::class,
            'assoc_foreign_key' => 'parent_id',
            'assoc_func' => 'findByParent',
            'on_delete' => 'delete',
            'on_store' => 'store',
        ];
        $config['has_and_belongs_to_many']['courses'] = [
            'thru_table' => 'seminar_sem_tree',
        ];
        $config['belongs_to']['_parent'] = [
            'class_name' => StudipStudyArea::class,
            'foreign_key' => 'parent_id',
        ];

        $config = self::registerCachableCallbacks($config);

        parent::configure($config);
    }

    /**
     * This is required, if the nodes are added backwards
     */
    public $required_children = [];

    /**
     * Returns the children of the study area with the specified ID.
     */
    public static function findByParent($parent_id)
    {
        return self::findByparent_id($parent_id, "ORDER BY priority,name");
    }

    /**
     * Returns the study area with the specified ID.
     */
    public static function find($id)
    {

        $result = NULL;

        if ($id === self::ROOT) {
            $result = self::getRootArea();
        }

        else {
            $result = parent::find($id);
        }

        return $result;
    }

    /**
     * Get a string representation of this study area.
     */
    public function __toString()
    {
        return $this->id;
    }


    /**
     * Get the comment of this study area.
     */
    public function getInfo()
    {
        return $this->content['info'];
    }


    /**
     * Set the comment of this study area.
     */
    public function setInfo($info)
    {
        $this->content['info'] = (string) $info;
        return $this;
    }


    /**
     * Get the display name of this study area.
     */
    public function getName(): string
    {
        return $this->content['name'];
    }

    /**
     * Set the display name of this study area.
     */
    public function setName($name)
    {
        $this->content['name'] = (string) $name;
        return $this;
    }


    /**
     * Get the parent ID of this study area.
     */
    public function getParentId()
    {
        return $this->content['parent_id'];
    }


    /**
     * Get the parent.
     */
    public function getParent()
    {
        $result = NULL;
        if ($this->getID() !== self::ROOT) {
            $result = $this->_parent;
        }
        return $result;
    }


    /**
     * Set the parent of this study area.
     */
    public function setParentId($parent_id)
    {
        $this->content['parent_id'] = (string) $parent_id;
        $this->resetRelation('parent');
        return $this;
    }

    /**
     * get the type of this study area.
     */
    public function getType()
    {
        return $this->content['type'];
    }

    /**
     * set the type of this study area.
     */
    public function setType($type)
    {
        $this->content['type'] = (int) $type;
        return $this;
    }

    /**
     * get the name of the type of this study area, see $SEM_TREE_TYPES in config.inc.php
     *
     * @return string
     */
    public function getTypeName()
    {
        if(isset($GLOBALS['SEM_TREE_TYPES'][$this->getType()]['name'])){
            return $GLOBALS['SEM_TREE_TYPES'][$this->getType()]['name'];
        } else {
            return '';
        }
    }

    /**
     * is this study area editable, see $SEM_TREE_TYPES in config.inc.php
     *
     * @return bool
     */
    public function isEditable()
    {
        if(isset($GLOBALS['SEM_TREE_TYPES'][$this->getType()]['editable'])){
            return (bool)$GLOBALS['SEM_TREE_TYPES'][$this->getType()]['editable'];
        } else {
            return false;
        }
    }

    /**
     * is this study area hidden, see $SEM_TREE_TYPES in config.inc.php
     *
     * @return bool
     */
    public function isHidden()
    {
        if (isset($GLOBALS['SEM_TREE_TYPES'][$this->getType()]['hidden'])) {
            return (bool) $GLOBALS['SEM_TREE_TYPES'][$this->getType()]['hidden'];
        } else {
            return false;
        }
    }

    /**
     * Get the path along the sem_tree to this study area.
     *
     * @param  string     optional; TODO
     *
     * @return mixed      TODO
     */
    public function getPath($separator = NULL)
    {

        $path = [];

        $area = $this;
        while ($area) {
            if ($area->getName() != '') {
                $path[] = $area->getName();
            }
            if ($area->getParentId() == self::ROOT) {
                break;
            }
            $area = $area->getParent();
        }

        $path = array_reverse($path);

        return isset($separator)
        ? join($separator, $path)
        : $path;
    }


    /**
     * Get the priority of this study area.
     */
    public function getPriority()
    {
        return $this->content['priority'];
    }


    /**
     * Set the priority of this study area.
     */
    public function setPriority($priority)
    {
        $this->content['priority'] = (int) $priority;
        return $this;
    }


    /**
     * Returns the children of this study area.
     */
    public function getChildren()
    {
        return $this->_children;
    }

    /**
     * Returns1 TRUE if the area has children.
     */
    public function hasChildren()
    {
        return sizeof($this->_children) > 0;
    }


    /**
     * Returns TRUE if this area is the root.
     */
    public function isRoot()
    {
        return $this->getId() === self::ROOT;
    }


    /**
     * Returns TRUE if this area can be select.
     */
    public function isAssignable()
    {
        $cfg = Config::GetInstance();
        $leaves_too = $cfg->getValue('SEM_TREE_ALLOW_BRANCH_ASSIGN');
        if ($leaves_too) {
            return !$this->isRoot() && !$this->isHidden();
        } else {
            return !$this->isRoot() && !$this->isHidden() && !$this->hasChildren();
        }
    }

    /**
     * is this study area considered a study modul?, see $SEM_TREE_TYPES in config.inc.php
     *
     * @return bool
     */
    public function isModule()
    {
        return isset($GLOBALS['SEM_TREE_TYPES'][$this->getType()]['is_module']);
    }

    /**
     * Get an associative array of all study areas of a course. The array
     * contains StudipStudyArea instances
     *
     * @param  id         the course's ID
     *
     * @return SimpleCollection      a SimpleORMapCollection of that course's study areas
     */
    public static function getStudyAreasForCourse($id)
    {
        $course = Course::find($id);
        return $course ? $course->study_areas : new SimpleCollection();
    }


    /**
     * Returns the not really existing root study area.
     *
     * @return object     the root study area object
     */
    public static function getRootArea()
    {
        $root = new StudipStudyArea();
        $root->setID(self::ROOT);
        $root->setName(Config::get()->UNI_NAME_CLEAN);
        return $root;
    }


    /**
     * Search for study areas whose name matches the given search term.
     *
     * @param string $searchTerm the seach term
     * @return StudipStudyArea[] nodes
     */
    public static function search($searchTerm)
    {
        return self::findBySql(
            "name LIKE :searchTerm ORDER BY priority",
            ['searchTerm' => "%$searchTerm%"]
        );
    }

    /**
     * Takes an array of StudyArea objects and produces the tree to the root node
     *
     * @param array $nodes All required nodes in the tree
     * @return StudipStudyArea the root node
     */
    public static function backwards($nodes)
    {
        // create the dummy root
        $root = static::getRootArea();

        $hashmap = [];

        $i = 0;

        // let the backwardssearch begin
        while ($nodes && $i < 99) {

            //clear cache
            $newNodes = [];

            //process nodes on this level
            foreach ($nodes as $node) {

                // if we know the node already place there
Moritz Strohm's avatar
Moritz Strohm committed
                if (isset($hashmap[$node->parent_id])) {
                    $cached = $hashmap[$node->parent_id];
                    $cached->required_children[$node->id] = $node;
                } else {
                    // if we have a node that is directly under root
                    if ($node->parent_id == $root->id) {
                        $root->required_children[$node->id] = $node;
                    } else {
                        // else store in hashmap and continue
                        $hashmap[$node->parent_id] = $node->_parent;
                        $node->_parent->required_children[$node->id] = $node;
                        $newNodes[$node->id] = $node->_parent;
                    }
                }
            }
            $nodes = $newNodes;
            $i++;
        }

        // plant the tree
        return $root;
    }

    public static function getNode($id): StudipTreeNode
    {
        if ($id === 'root') {
            return static::build([
                'id'   => 'root',
                'name' => Config::get()->UNI_NAME_CLEAN,
            ]);
        }

        return static::find($id);
    }

    public static function getCourseNodes(string $course_id): array
    {
        return Course::find($course_id)->study_areas->getArrayCopy();
    }

    public function getDescription(): string
    {
        return $this->getInfo();
    }

    /**
     * @see StudipTreeNode::getImage()
     */
    public function getImage()
    {
        return null;
    }

    public function hasChildNodes(): bool
    {
        return count($this->_children) > 0;
    }

    /**
     * @see StudipTreeNode::getChildNodes()
     */
    public function getChildNodes(bool $onlyVisible = false): array
    {
        if ($onlyVisible) {
            $visibleTypes = array_filter($GLOBALS['SEM_TREE_TYPES'], function ($t) {
                return isset($t['hidden']) ? !$t['hidden'] : true;
            });

            return static::findBySQL(
                "`parent_id` = :parent AND `type` IN (:types) ORDER BY `priority`, `name`",
                ['parent' => $this->id, 'types' => $visibleTypes]
            );
        } else {
            return static::findByParent_id($this->id, "ORDER BY `priority`, `name`");
        }
    }

    /**
     * @see StudipTreeNode::countCourses()
     */
    public function countCourses($semester_id = 'all', $semclass = 0, $with_children = false) :int
    {
        if ($semester_id !== 'all') {
            $query = "SELECT COUNT(DISTINCT t.`seminar_id`)
                      FROM `seminar_sem_tree` t
                      JOIN `seminare` s ON (s.`Seminar_id` = t.`seminar_id`)
                      LEFT JOIN `semester_courses` sc ON (t.`seminar_id` = sc.`course_id`)
                      WHERE t.`sem_tree_id` IN (:ids)
                        AND (
                          sc.`semester_id` = :semester
                          OR sc.`semester_id` IS NULL
                        )";
            $parameters = [
                'ids' => $with_children ? $this->getDescendantIds() : [$this->id],
                'semester' => $semester_id
            ];
        } else {
            $query = "SELECT COUNT(DISTINCT t.`seminar_id`)
                      FROM `seminar_sem_tree` t
                      JOIN `seminare` s ON (s.`Seminar_id` = t.`seminar_id`)
                      WHERE `sem_tree_id` IN (:ids)";
            $parameters = ['ids' => $with_children ? $this->getDescendantIds() : [$this->id]];
        }

        if (!$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM)) {
            $query .= " AND s.`visible` = 1";
        }

        if ($semclass !== 0) {
            $query .= "  AND s.`status` IN (:types)";
            $parameters['types'] = array_map(
                function ($type) {
                    return $type['id'];
                },
                array_filter(
                    SemType::getTypes(),
                    function ($t) use ($semclass) { return $t['class'] === $semclass; }
                )
            );
        }

        return $this->id === 'root' && !$with_children ? 0 : DBManager::get()->fetchColumn($query, $parameters);
    }

    public function getCourses(
        $semester_id = 'all',
        $semclass = 0,
        $searchterm = '',
        $with_children = false,
        array $courses = []
    ): array
    {
        if ($semester_id !== 'all') {
            $query = "SELECT DISTINCT s.*
                      FROM `seminare` s
                      JOIN `seminar_sem_tree` t ON (t.`seminar_id` = s.`Seminar_id`)
                      LEFT JOIN `semester_courses` sem ON (sem.`course_id` = s.`Seminar_id`)
                      WHERE t.`sem_tree_id` IN (:ids)
                        AND (
                          sem.`semester_id` = :semester
                          OR sem.`semester_id` IS NULL
                        )";
            $parameters = [
                'ids' => $with_children ? $this->getDescendantIds() : [$this->id],
                'semester' => $semester_id
            ];
        } else {
            $query = "SELECT DISTINCT s.*
                      FROM `seminare` s
                      JOIN `seminar_sem_tree` t ON (t.`seminar_id` = s.`Seminar_id`)
                      WHERE t.`sem_tree_id` IN (:ids)";
            $parameters = ['ids' => $with_children ? $this->getDescendantIds() : [$this->id]];
        }

        if (!$GLOBALS['perm']->have_perm(Config::get()->SEM_VISIBILITY_PERM)) {
            $query .= " AND s.`visible` = 1";
        }

        if ($semclass !== 0) {
            $query .= "  AND s.`status` IN (:types)";
            $parameters['types'] = array_map(
                function ($type) {
                    return $type['id'];
                },
                array_filter(
                    SemType::getTypes(),
                    function ($t) use ($semclass) { return $t['class'] === $semclass; }
                )
            );
        }

        if ($searchterm) {
            $query .= " AND s.`Name` LIKE :searchterm";
            $parameters['searchterm'] = '%' . trim($searchterm) . '%';
        }

        if ($courses) {
            $query .= " AND t.`seminar_id` IN (:courses)";
            $parameters['courses'] = $courses;
        }

        if (Config::get()->IMPORTANT_SEMNUMBER) {
            $query .= " ORDER BY s.`start_time`, s.`VeranstaltungsNummer`, s.`Name`";
        } else {
            $query .= " ORDER BY s.`start_time`, s.`Name`";
        }

        return DBManager::get()->fetchAll($query, $parameters, 'Course::buildExisting');
    }

    public function getAncestors(): array
    {
        $path = [
            [
                'id' => $this->id,
                'name' => $this->getName(),
                'classname' => static::class
            ]
        ];

        if ($this->parent_id) {
            $path = array_merge($this->getNode($this->parent_id)->getAncestors(), $path);
        }

        return $path;
    }

    /**
     * Constructs an index from the level hierarchy, This index is a number,
     * containing the "depth" level and the priority on this level. For example,
     * a node on level 2 with priority 3 will get an index of 23.
     *
     * @return int
     */
    public function getIndex()
    {
        $level = 1;
        $index = (string) $level . (string) $this->priority;
        $current = $this;

        while ($current->getParent()) {
            $current = $current->getParent();
            $index .= $level . $current->priority;
            $level++;
        }

        return $index;
    }